From cd69c1c02b2e4674731178def7d217d5699bde25 Mon Sep 17 00:00:00 2001 From: Ron Spickenagel Date: Tue, 13 Jun 2023 17:47:22 -0400 Subject: [PATCH] refactor!: Rewrote for new major v3 (see detail) - Added Live Compiler (on-the-fly, in-memory patching) - Added experimental ES Module support (closes #58) - Added mutex locks (closes #75) - Updated to support TS v5+ (closes #83 closes #93) - Fixed patching for non-standard libraries (cannot guarantee they will work as expected in IDEs) (closes #85) - Added caching --- .editorconfig | 17 +- .github/workflows/build.yml | 27 +- .gitignore | 1 - .idea/inspectionProfiles/Project_Default.xml | 8 +- .idea/ts-patch.iml | 6 +- README.md | 345 +++---- jest.config.ts | 13 +- package.json | 80 +- {src => projects/core}/plugin.ts | 8 +- projects/core/resolver-hook.js | 55 ++ {src => projects/core}/shared/plugin-types.ts | 19 +- projects/core/src/actions/check.ts | 70 ++ projects/core/src/actions/index.ts | 5 + projects/core/src/actions/install.ts | 23 + projects/core/src/actions/patch.ts | 98 ++ projects/core/src/actions/uninstall.ts | 24 + projects/core/src/actions/unpatch.ts | 102 +++ projects/core/src/bin/ts-patch.ts | 7 + .../core/src/bin/tspc.ts | 11 +- projects/core/src/cli/cli.ts | 94 ++ projects/core/src/cli/commands.ts | 55 ++ projects/core/src/cli/help-menu.ts | 48 + projects/core/src/cli/options.ts | 83 ++ projects/core/src/compiler/package.json | 4 + projects/core/src/compiler/tsc.js | 19 + projects/core/src/compiler/tsserver.js | 19 + projects/core/src/compiler/tsserverlibrary.js | 19 + projects/core/src/compiler/typescript.js | 19 + projects/core/src/config.ts | 69 ++ projects/core/src/index.ts | 4 + projects/core/src/module/get-live-module.ts | 25 + projects/core/src/module/index.ts | 4 + projects/core/src/module/module-file.ts | 121 +++ projects/core/src/module/module-source.ts | 54 ++ projects/core/src/module/source-section.ts | 110 +++ projects/core/src/module/ts-module.ts | 139 +++ projects/core/src/options.ts | 55 ++ projects/core/src/patch/get-patched-source.ts | 85 ++ projects/core/src/patch/patch-detail.ts | 106 +++ projects/core/src/patch/patch-module.ts | 197 ++++ .../add-original-create-program.ts | 68 ++ .../patch/transformers/fix-ts-early-return.ts | 46 + .../src/patch/transformers/hook-tsc-exec.ts | 52 ++ projects/core/src/patch/transformers/index.ts | 6 + .../patch/transformers/merge-statements.ts | 61 ++ .../transformers/patch-create-program.ts | 68 ++ .../src/patch/transformers/patch-emitter.ts | 75 ++ projects/core/src/slice/module-slice.ts | 43 + projects/core/src/slice/ts5.ts | 72 ++ projects/core/src/system/cache.ts | 48 + .../core/src}/system/errors.ts | 22 +- projects/core/src/system/index.ts | 4 + projects/core/src/system/logger.ts | 53 ++ projects/core/src/system/types.ts | 11 + projects/core/src/ts-package.ts | 99 ++ projects/core/src/utils/file-utils.ts | 104 +++ projects/core/src/utils/find-cache-dir.ts | 122 +++ projects/core/src/utils/general.ts | 24 + projects/core/src/utils/index.ts | 3 + projects/core/tsconfig.json | 19 + {src => projects}/patch/package.json | 0 {src => projects}/patch/plugin.ts | 36 +- projects/patch/src/plugin/esm-intercept.ts | 124 +++ projects/patch/src/plugin/plugin-creator.ts | 201 ++++ projects/patch/src/plugin/register-plugin.ts | 139 +++ projects/patch/src/plugin/resolve-factory.ts | 117 +++ projects/patch/src/shared.ts | 54 ++ .../patch/src/ts/create-program.ts | 78 +- projects/patch/src/ts/shim.ts | 48 + .../patch/src}/types/plugin-types.ts | 4 +- projects/patch/src/types/typescript.ts | 11 + {src => projects}/patch/tsconfig.json | 8 +- scripts/postbuild.js | 26 +- src/installer/bin/cli.ts | 165 ---- src/installer/index.ts | 4 - src/installer/lib/actions.ts | 267 ------ src/installer/lib/file-utils.ts | 210 ----- src/installer/lib/patcher.ts | 157 ---- src/installer/lib/system/helpers.ts | 76 -- src/installer/lib/system/index.ts | 4 - src/installer/lib/system/logger.ts | 45 - src/installer/lib/system/options.ts | 63 -- src/patch/lib/main.ts | 10 - src/patch/lib/plugin.ts | 247 ----- src/patch/lib/shared.ts | 26 - src/patch/lib/types/typescript.ts | 41 - src/tsconfig.json | 22 - test/assets/projects/main/package.json | 5 + test/assets/projects/main/src/index.ts | 0 test/assets/projects/main/tsconfig.json | 16 + test/assets/projects/path-mapping/base.ts | 1 + .../assets/projects/path-mapping/package.json | 8 + test/assets/projects/path-mapping/src/a/a.ts | 1 + test/assets/projects/path-mapping/src/b.ts | 1 + .../assets/projects/path-mapping/src/index.ts | 29 + .../projects/path-mapping/tsconfig.json | 17 + .../path-mapping/tsconfig.plugin.json | 14 + test/assets/projects/transform/package.json | 8 + .../projects/transform/run-transform.js | 41 + test/assets/projects/transform/src/index.ts | 4 + .../transform/transformers/cjs/js-plugin.cjs | 19 + .../transform/transformers/cjs/package.json | 3 + .../transform/transformers/cjs/plugin.cts | 17 + .../transform/transformers/cjs/tsconfig.json | 7 + .../transform/transformers/esm/js-plugin.mjs | 17 + .../transform/transformers/esm/package.json | 3 + .../transform/transformers/esm/plugin.mts | 19 + .../transform/transformers/esm/plugin.ts | 19 + .../transform/transformers/esm/tsconfig.json | 7 + .../projects/transform/tsconfig.cjs.json | 12 + .../projects/transform/tsconfig.cts.json | 12 + test/assets/projects/transform/tsconfig.json | 9 + .../projects/transform/tsconfig.mjs.json | 12 + .../projects/transform/tsconfig.mts.json | 11 + .../projects/transform/tsconfig.ts.json | 11 + test/assets/projects/webpack/esm-plugin.mjs | 5 + test/assets/projects/webpack/esm-plugin.mts | 5 + test/assets/projects/webpack/hide-module.js | 24 + test/assets/projects/webpack/package.json | 14 + test/assets/projects/webpack/plugin.ts | 7 + test/assets/projects/webpack/src/index.ts | 0 .../assets/projects/webpack/tsconfig.esm.json | 8 + .../projects/webpack/tsconfig.esmts.json | 8 + test/assets/projects/webpack/tsconfig.json | 13 + .../assets/projects/webpack/webpack.config.js | 24 + .../assets/src-files/mapping-project/index.ts | 1 - .../mapping-project/transformer/a.ts | 4 - .../transformer/transformer.ts | 2 - .../mapping-project/transformer/tsconfig.json | 11 - .../src-files/mapping-project/tsconfig.json | 13 - .../mapping-project/tsconfig.noproject.json | 10 - test/assets/src-files/safely-code.ts | 7 - .../src-files/tsconfig.alter-diags.json | 11 - test/assets/src-files/tsconfig.json | 10 - test/assets/src-files/tsnode-code.ts | 8 - test/assets/test-package.json | 8 - test/assets/transformers/alter-diagnostics.ts | 45 - test/assets/transformers/before.ts | 21 - .../transformers/program-transformer.ts | 66 -- test/assets/transformers/safely.ts | 34 - .../assets/transformers/transform-advanced.ts | 13 - test/assets/transformers/transform-simple.ts | 11 - test/assets/transformers/transformers.ts | 7 - test/package.json | 8 +- test/src/cleanup.ts | 10 + test/src/config.ts | 9 +- test/src/perf.ts | 110 +++ test/src/prepare.ts | 10 + test/src/project.ts | 134 +++ test/src/setup.js | 26 - test/src/utils.ts | 102 --- test/src/utils/general.ts | 8 + test/tests/actions.test.ts | 269 ++++++ test/tests/installer/actions.test.ts | 212 ----- test/tests/installer/cli.test.ts | 188 ---- test/tests/installer/logger.test.ts | 68 -- test/tests/patch/plugin-creator.test.ts | 86 -- test/tests/patch/tsc.test.ts | 197 ---- test/tests/patch/typescript.test.ts | 273 ------ test/tests/path-mapping.test.ts | 47 + test/tests/transformer.test.ts | 40 + test/tests/webpack.test.ts | 63 ++ test/tsconfig.json | 2 +- tsconfig.base.json | 4 +- yarn.lock | 858 ++++++++---------- 165 files changed, 5241 insertions(+), 3627 deletions(-) rename {src => projects/core}/plugin.ts (81%) create mode 100644 projects/core/resolver-hook.js rename {src => projects/core}/shared/plugin-types.ts (94%) create mode 100644 projects/core/src/actions/check.ts create mode 100644 projects/core/src/actions/index.ts create mode 100644 projects/core/src/actions/install.ts create mode 100644 projects/core/src/actions/patch.ts create mode 100644 projects/core/src/actions/uninstall.ts create mode 100644 projects/core/src/actions/unpatch.ts create mode 100644 projects/core/src/bin/ts-patch.ts rename test/src/teardown.js => projects/core/src/bin/tspc.ts (56%) create mode 100644 projects/core/src/cli/cli.ts create mode 100644 projects/core/src/cli/commands.ts create mode 100644 projects/core/src/cli/help-menu.ts create mode 100644 projects/core/src/cli/options.ts create mode 100644 projects/core/src/compiler/package.json create mode 100644 projects/core/src/compiler/tsc.js create mode 100644 projects/core/src/compiler/tsserver.js create mode 100644 projects/core/src/compiler/tsserverlibrary.js create mode 100644 projects/core/src/compiler/typescript.js create mode 100644 projects/core/src/config.ts create mode 100644 projects/core/src/index.ts create mode 100644 projects/core/src/module/get-live-module.ts create mode 100644 projects/core/src/module/index.ts create mode 100644 projects/core/src/module/module-file.ts create mode 100644 projects/core/src/module/module-source.ts create mode 100644 projects/core/src/module/source-section.ts create mode 100644 projects/core/src/module/ts-module.ts create mode 100644 projects/core/src/options.ts create mode 100644 projects/core/src/patch/get-patched-source.ts create mode 100644 projects/core/src/patch/patch-detail.ts create mode 100644 projects/core/src/patch/patch-module.ts create mode 100644 projects/core/src/patch/transformers/add-original-create-program.ts create mode 100644 projects/core/src/patch/transformers/fix-ts-early-return.ts create mode 100644 projects/core/src/patch/transformers/hook-tsc-exec.ts create mode 100644 projects/core/src/patch/transformers/index.ts create mode 100644 projects/core/src/patch/transformers/merge-statements.ts create mode 100644 projects/core/src/patch/transformers/patch-create-program.ts create mode 100644 projects/core/src/patch/transformers/patch-emitter.ts create mode 100644 projects/core/src/slice/module-slice.ts create mode 100644 projects/core/src/slice/ts5.ts create mode 100644 projects/core/src/system/cache.ts rename {src/installer/lib => projects/core/src}/system/errors.ts (54%) create mode 100644 projects/core/src/system/index.ts create mode 100644 projects/core/src/system/logger.ts create mode 100644 projects/core/src/system/types.ts create mode 100644 projects/core/src/ts-package.ts create mode 100644 projects/core/src/utils/file-utils.ts create mode 100644 projects/core/src/utils/find-cache-dir.ts create mode 100644 projects/core/src/utils/general.ts create mode 100644 projects/core/src/utils/index.ts create mode 100644 projects/core/tsconfig.json rename {src => projects}/patch/package.json (100%) rename {src => projects}/patch/plugin.ts (74%) create mode 100644 projects/patch/src/plugin/esm-intercept.ts create mode 100644 projects/patch/src/plugin/plugin-creator.ts create mode 100644 projects/patch/src/plugin/register-plugin.ts create mode 100644 projects/patch/src/plugin/resolve-factory.ts create mode 100644 projects/patch/src/shared.ts rename src/patch/lib/createProgram.ts => projects/patch/src/ts/create-program.ts (59%) create mode 100644 projects/patch/src/ts/shim.ts rename {src/patch/lib => projects/patch/src}/types/plugin-types.ts (96%) create mode 100644 projects/patch/src/types/typescript.ts rename {src => projects}/patch/tsconfig.json (83%) delete mode 100644 src/installer/bin/cli.ts delete mode 100644 src/installer/index.ts delete mode 100644 src/installer/lib/actions.ts delete mode 100644 src/installer/lib/file-utils.ts delete mode 100644 src/installer/lib/patcher.ts delete mode 100644 src/installer/lib/system/helpers.ts delete mode 100644 src/installer/lib/system/index.ts delete mode 100644 src/installer/lib/system/logger.ts delete mode 100644 src/installer/lib/system/options.ts delete mode 100644 src/patch/lib/main.ts delete mode 100644 src/patch/lib/plugin.ts delete mode 100644 src/patch/lib/shared.ts delete mode 100644 src/patch/lib/types/typescript.ts delete mode 100644 src/tsconfig.json create mode 100644 test/assets/projects/main/package.json create mode 100644 test/assets/projects/main/src/index.ts create mode 100644 test/assets/projects/main/tsconfig.json create mode 100644 test/assets/projects/path-mapping/base.ts create mode 100644 test/assets/projects/path-mapping/package.json create mode 100644 test/assets/projects/path-mapping/src/a/a.ts create mode 100644 test/assets/projects/path-mapping/src/b.ts create mode 100644 test/assets/projects/path-mapping/src/index.ts create mode 100644 test/assets/projects/path-mapping/tsconfig.json create mode 100644 test/assets/projects/path-mapping/tsconfig.plugin.json create mode 100644 test/assets/projects/transform/package.json create mode 100644 test/assets/projects/transform/run-transform.js create mode 100644 test/assets/projects/transform/src/index.ts create mode 100644 test/assets/projects/transform/transformers/cjs/js-plugin.cjs create mode 100644 test/assets/projects/transform/transformers/cjs/package.json create mode 100644 test/assets/projects/transform/transformers/cjs/plugin.cts create mode 100644 test/assets/projects/transform/transformers/cjs/tsconfig.json create mode 100644 test/assets/projects/transform/transformers/esm/js-plugin.mjs create mode 100644 test/assets/projects/transform/transformers/esm/package.json create mode 100644 test/assets/projects/transform/transformers/esm/plugin.mts create mode 100644 test/assets/projects/transform/transformers/esm/plugin.ts create mode 100644 test/assets/projects/transform/transformers/esm/tsconfig.json create mode 100644 test/assets/projects/transform/tsconfig.cjs.json create mode 100644 test/assets/projects/transform/tsconfig.cts.json create mode 100644 test/assets/projects/transform/tsconfig.json create mode 100644 test/assets/projects/transform/tsconfig.mjs.json create mode 100644 test/assets/projects/transform/tsconfig.mts.json create mode 100644 test/assets/projects/transform/tsconfig.ts.json create mode 100644 test/assets/projects/webpack/esm-plugin.mjs create mode 100644 test/assets/projects/webpack/esm-plugin.mts create mode 100644 test/assets/projects/webpack/hide-module.js create mode 100644 test/assets/projects/webpack/package.json create mode 100644 test/assets/projects/webpack/plugin.ts create mode 100644 test/assets/projects/webpack/src/index.ts create mode 100644 test/assets/projects/webpack/tsconfig.esm.json create mode 100644 test/assets/projects/webpack/tsconfig.esmts.json create mode 100644 test/assets/projects/webpack/tsconfig.json create mode 100644 test/assets/projects/webpack/webpack.config.js delete mode 100644 test/assets/src-files/mapping-project/index.ts delete mode 100644 test/assets/src-files/mapping-project/transformer/a.ts delete mode 100644 test/assets/src-files/mapping-project/transformer/transformer.ts delete mode 100644 test/assets/src-files/mapping-project/transformer/tsconfig.json delete mode 100644 test/assets/src-files/mapping-project/tsconfig.json delete mode 100644 test/assets/src-files/mapping-project/tsconfig.noproject.json delete mode 100644 test/assets/src-files/safely-code.ts delete mode 100644 test/assets/src-files/tsconfig.alter-diags.json delete mode 100644 test/assets/src-files/tsconfig.json delete mode 100644 test/assets/src-files/tsnode-code.ts delete mode 100644 test/assets/test-package.json delete mode 100644 test/assets/transformers/alter-diagnostics.ts delete mode 100644 test/assets/transformers/before.ts delete mode 100644 test/assets/transformers/program-transformer.ts delete mode 100644 test/assets/transformers/safely.ts delete mode 100644 test/assets/transformers/transform-advanced.ts delete mode 100644 test/assets/transformers/transform-simple.ts delete mode 100644 test/assets/transformers/transformers.ts create mode 100644 test/src/cleanup.ts create mode 100644 test/src/perf.ts create mode 100644 test/src/prepare.ts create mode 100644 test/src/project.ts delete mode 100644 test/src/setup.js delete mode 100644 test/src/utils.ts create mode 100644 test/src/utils/general.ts create mode 100644 test/tests/actions.test.ts delete mode 100644 test/tests/installer/actions.test.ts delete mode 100644 test/tests/installer/cli.test.ts delete mode 100644 test/tests/installer/logger.test.ts delete mode 100644 test/tests/patch/plugin-creator.test.ts delete mode 100644 test/tests/patch/tsc.test.ts delete mode 100644 test/tests/patch/typescript.test.ts create mode 100644 test/tests/path-mapping.test.ts create mode 100644 test/tests/transformer.test.ts create mode 100644 test/tests/webpack.test.ts diff --git a/.editorconfig b/.editorconfig index 144ae5f..cfc794f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,8 +22,7 @@ ij_editorconfig_space_before_colon = false ij_editorconfig_space_before_comma = false ij_editorconfig_spaces_around_assignment_operators = true - -[{*.js, *.cjs}] +[{*.js,*.cjs}] tab_width = 2 ij_continuation_indent_size = 2 ij_javascript_align_imports = false @@ -34,7 +33,7 @@ ij_javascript_align_multiline_extends_list = false ij_javascript_align_multiline_for = true ij_javascript_align_multiline_parameters = true ij_javascript_align_multiline_parameters_in_calls = false -ij_javascript_align_multiline_ternary_operation = true +ij_javascript_align_multiline_ternary_operation = false ij_javascript_align_object_properties = 0 ij_javascript_align_union_types = false ij_javascript_align_var_statements = 0 @@ -177,7 +176,6 @@ ij_javascript_ternary_operation_wrap = off ij_javascript_union_types_wrap = on_every_item ij_javascript_use_chained_calls_group_indents = false ij_javascript_use_double_quotes = true -ij_javascript_use_explicit_js_extension = auto ij_javascript_use_path_mapping = always ij_javascript_use_public_modifier = false ij_javascript_use_semicolon_after_statement = true @@ -187,7 +185,7 @@ ij_javascript_while_on_new_line = false ij_javascript_wrap_comments = false -[{*.zsh, *.bash, *.sh}] +[{*.zsh,*.bash,*.sh}] ij_shell_binary_ops_start_line = false ij_shell_keep_column_alignment_padding = false ij_shell_minify_program = false @@ -195,7 +193,7 @@ ij_shell_redirect_followed_by_space = false ij_shell_switch_cases_indented = false -[{.babelrc, .prettierrc, .stylelintrc, .eslintrc, jest.config, *.json, *.jsb3, *.jsb2, *.bowerrc}] +[{.babelrc,.prettierrc,.stylelintrc,.eslintrc,jest.config,*.json,*.jsb3,*.jsb2,*.bowerrc}] ij_json_keep_blank_lines_in_code = 2 ij_json_keep_indents_on_empty_lines = false ij_json_keep_line_breaks = true @@ -207,7 +205,7 @@ ij_json_spaces_within_braces = true ij_json_spaces_within_brackets = true ij_json_wrap_long_lines = false -[{*.ats, *.ts, *.tsx}] +[{*.ats,*.ts,*.tsx}] tab_width = 2 ij_continuation_indent_size = 2 ij_typescript_align_imports = false @@ -218,7 +216,7 @@ ij_typescript_align_multiline_extends_list = false ij_typescript_align_multiline_for = true ij_typescript_align_multiline_parameters = true ij_typescript_align_multiline_parameters_in_calls = false -ij_typescript_align_multiline_ternary_operation = true +ij_typescript_align_multiline_ternary_operation = false ij_typescript_align_object_properties = 0 ij_typescript_align_union_types = false ij_typescript_align_var_statements = 0 @@ -364,11 +362,10 @@ ij_typescript_ternary_operation_wrap = off ij_typescript_union_types_wrap = on_every_item ij_typescript_use_chained_calls_group_indents = false ij_typescript_use_double_quotes = false -ij_typescript_use_explicit_js_extension = auto ij_typescript_use_path_mapping = always ij_typescript_use_public_modifier = false ij_typescript_use_semicolon_after_statement = true ij_typescript_var_declaration_wrap = normal ij_typescript_while_brace_force = never ij_typescript_while_on_new_line = false -ij_typescript_wrap_comments = true +ij_typescript_wrap_comments = false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76eb8a9..0f6a1d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,12 @@ name: Build -on: [ push ] +on: + pull_request: + push: + branches: + - master + - v3-beta + - next jobs: build: @@ -8,17 +14,27 @@ jobs: strategy: matrix: - node-version: [ 12.x, 14.x, 16.x ] + node-version: [ 16.x, 18.x, 19.x ] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cache/yarn + node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install Packages run: yarn install --frozen-lockfile @@ -26,8 +42,7 @@ jobs: run: yarn build - name: Test - run: yarn run test + run: yarn run test && yarn run perf env: CI: true FORCE_COLOR: 1 - NODE_OPTIONS: --max-old-space-size=4096 diff --git a/.gitignore b/.gitignore index 7c603b9..133099a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ # Built *.d.ts *.js.map -*.js generator.stats.json .nyc_output dist diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e764891..cc4a2bc 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -10,13 +10,13 @@ - + - + - + @@ -95,4 +95,4 @@ - \ No newline at end of file + diff --git a/.idea/ts-patch.iml b/.idea/ts-patch.iml index bef6917..ebe1738 100644 --- a/.idea/ts-patch.iml +++ b/.idea/ts-patch.iml @@ -7,12 +7,14 @@ - - + + + + diff --git a/README.md b/README.md index 234e250..3a1542c 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ [![NPM Downloads](https://img.shields.io/npm/dm/ts-patch.svg?style=flat)](https://npmjs.org/package/ts-patch) ![Build Status](https://github.com/nonara/ts-patch/workflows/Build/badge.svg) -# TS Patch +# ts-patch -Directly patch typescript installation to allow custom transformers (plugins). +Patch typescript to allow custom transformers (plugins) during build. -- Plugins are specified in `tsconfig.json`, or provided programmatically in `CompilerOptions`. -- Based on [ttypescript](https://github.com/cevek/ttypescript) - Fully compatible + offers more features +Plugins are specified in `tsconfig.json`, or provided programmatically in `CompilerOptions`. + +_Migrating from ttypescript is easy! See: [Method 1: Live Compiler](#method-1-live-compiler)_ ## TypeScript v5 Note @@ -18,25 +19,76 @@ We're working on adding support. More notes on that here: - [Issue #93 — Not working with TypeScript v5 (author's note)](https://github.com/nonara/ts-patch/issues/93) ## Features -* Patch / unpatch any version of typescript (4.*) -* Advanced options for patching individual libraries, specific locations, etc. (see `ts-patch /?`) -* _(New)_ Supports 'transforming' the `Program` instance during creation. (see: [Transforming Program](#transforming-program)) -* _(New)_ Add, remove, or modify diagnostics! (see: [Altering Diagnostics](#altering-diagnostics)) -## Setup +* Patch typescript installation via on-the-fly, in-memory patching _or_ as a persistent patch +* Can patch individual libraries (see `ts-patch /?`) +* Hook build process by transforming the `Program` (see: [Transforming Program](#transforming-program)) +* Add, remove, or modify diagnostics (see: [Altering Diagnostics](#altering-diagnostics)) +* Fully compatible with legacy [ttypescript](https://github.com/cevek/ttypescript) projects +* **(new)** Experimental support for ES Module based transformers + +# Table of Contents + + +* [ts-patch](#ts-patch) + * [Features](#features) +* [Table of Contents](#table-of-contents) +* [Installation](#installation) +* [Usage](#usage) + * [Method 1: Live Compiler](#method-1-live-compiler) + * [Method 2: Persistent Patch](#method-2-persistent-patch) +* [Configuration](#configuration) + * [Plugin Options](#plugin-options) +* [Writing Transformers](#writing-transformers) + * [Source Transformers](#source-transformers) + * [Source Transformer Entry Point](#source-transformer-entry-point) + * [Source Transformer Example](#source-transformer-example) + * [Altering Diagnostics](#altering-diagnostics) + * [Note](#note) + * [Program Transformers](#program-transformers) + * [Program Transformer Entry Point](#program-transformer-entry-point) + * [Configuring Program Transformers](#configuring-program-transformers) + * [Example Program Transformer](#example-program-transformer) + * [Resources](#resources) + * [Recommended Reading](#recommended-reading) + * [Recommended Tools](#recommended-tools) + * [Discussion](#discussion) +* [Advanced Options](#advanced-options) +* [Maintainers](#maintainers) + * [Help Wanted](#help-wanted) +* [License](#license) + + +# Installation 1. Install package ```sh add -D ts-patch ``` -2. Patch typescript +# Usage + +## Method 1: Live Compiler + +The live compiler patches on-the-fly each it is run. + +**Via commandline:** Simply use `tspc` (instead of `tsc`) + +**With tools such as ts-node, webpack, etc:** specify the compiler as `ts-patch/compiler` + +## Method 2: Persistent Patch + +Persistent patch modifies the typescript installation within the node_modules path. It requires additional configuration +to remain persisted, but it carries less load time and complexity compared to the live compiler. + +1. Install the patch + ```shell # For advanced options, see: ts-patch /? ts-patch install ``` -3. Add `prepare` script (keeps patch persisted after npm install) +2. Add `prepare` script (keeps patch persisted after npm install) `package.json` ```jsonc @@ -47,159 +99,116 @@ ts-patch install } } ``` + +# Configuration -## Table of Contents - - [Configuring](#configuring) - - [tsconfig.json](#tsconfigjson) - - [Plugin Options](#plugin-options) - - [Source Transformer Signatures](#source-transformer-signatures) - - [Usage](#usage) - - [Transforming AST Nodes](#transforming-ast-nodes) - - [Example Node Transformers](#example-node-transformers) - - [Transforming Program](#transforming-program) - - [Example Program Transformer](#example-program-transformer) - - [Altering Diagnostics](#altering-diagnostics) - - [Resources](#resources) - - [Recommended Reading](#recommended-reading) - - [Recommended Tools](#recommended-tools) - - [Credit](#credit) - - [HALP!!!](#halp) - - [License](#license) - -## Configuring -### tsconfig.json - -Add transformers to `compilerOptions` in `plugins` array. +**tsconfig.json**: Add transformers to `compilerOptions` in `plugins` array. **Examples** ```jsonc { "compilerOptions": { "plugins": [ - // Source Transformer: 'type' defaults to 'program' - { "transform": "transformer-module", "someOption1": 123, "someOption2": 321 }, - - // Source Transformer: program signature - { "transform": "./transformers/my-transformer.ts", "type": "program" }, - - // Source Transformer: program signature, applies after TS transformers - { "transform": "transformer-module1", "type": "config", "after": true }, + // Source Transformers + { "transform": "transformer-module" }, + { "transform": "transformer2", "extraOption": 123 }, + { "transform": "trans-with-mapping", "resolvePathAliases": true }, + { "transform": "esm-transformer, "isEsm": true }, - // Source Transformer: checker signature, applies to TS declarations - { "transform": "transformer-module2", "type": "checker", "afterDeclarations": true }, - - // Source Transformer: raw signature - { "transform": "transformer-module3", "type": "raw" }, - - // Source Transformer: compilerOptions signature - { "transform": "transformer-module4", "type": "compilerOptions" }, - - // Program Transformer: Only has one signature - no type specified, because it does not apply + // Program Transformer { "transform": "transformer-module5", "transformProgram": true } ] } } ``` -### Plugin Options -| Option | Type | Description | -| ------------------| ------- | :----------- | -| **transform** | string | Module name or path to transformer _(*.ts or *.js)_ | -| type | string | *Source Transformer* entry point signature _(see: [Source Transformer Signatures](#source-transformer-signatures))_ | -| import | string | Name of exported transformer function _(defaults to `default` export)_ | -| tsConfig | string | tsconfig.json file _for transformer_ (allows specifying compileOptions, path mapping support, etc) | -| after | boolean | Apply transformer after stock TS transformers. | -| afterDeclarations | boolean | Apply transformer to declaration (*.d.ts) files _(TypeScript 2.9+)_. | -| transformProgram | boolean | Transform `Program` during `ts.createProgram()` _(see: [Transforming Program](#transforming-program))_ | -| _..._ | | Provide your own custom options, which will be passed to the transformer | +## Plugin Options + +| Option | Type | Description | +|--------------------|---------|:--------------------------------------------------------------------------------------------------------------| +| **transform** | string | Module name or path to transformer _(*.ts or *.js)_ | +| after | boolean | Apply transformer after stock TS transformers | +| afterDeclarations | boolean | Apply transformer to declaration (*.d.ts) files | +| transformProgram | boolean | Transform `Program` during `ts.createProgram()` _(see: [Program Transformers](#program-transformers))_ | +| isEsm | boolean | Transformer is ES Module (_note: experimental_ — requires [esm](https://www.npmjs.com/package/esm)) | +| resolvePathAliases | boolean | Resolve path aliases in transformer (requires [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths)) | +| type | string | See: [Source Transformer Entry Point](#source-transformer-entry-point) (default: 'program') | +| import | string | Name of exported transformer function _(defaults to `default` export)_ | +| tsConfig | string | tsconfig.json file _for transformer_ (allows specifying compileOptions, path mapping support, etc) | +| _..._ | | Provide your own custom options, which will be passed to the transformer | _Note: Required options are bold_ -### Source Transformer Signatures -The following are the possible values for the `type` option and their corresponding entry point signatures. -_Note: These apply to Source Transformers only._ - -#### program (default) +# Writing Transformers -Signature with `ts.Program` instance: -```ts -(program: ts.Program, config: PluginConfig, extras: TransformerExtras) => ts.TransformerFactory -``` +## Source Transformers -_ts.TransformerFactory_ >>> `(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile` -_TransformerExtras_ >>> [See Type Declaration](https://github.com/nonara/ts-patch/blob/master/src/installer/plugin-types.ts#L76) +Source Transformers will transform the AST of SourceFiles during compilation, allowing you to alter the output of the JS or declarations files. -_Note: This is *not* the configuration for a [Program Transformer](#transforming-program)._ +### Source Transformer Entry Point -#### config -Signature with transformer's config: ```ts -(config: PluginConfig) => ts.TransformerFactory -``` - -#### checker -Signature with `ts.TypeChecker`: -```ts -(checker: ts.TypeChecker, config: PluginConfig) => ts.TransformerFactory +(program: ts.Program, config: PluginConfig, extras: TransformerExtras) => ts.TransformerFactory ``` -#### raw -Signature without `ts-patch` wrapper: -```ts -/* ts.TransformerFactory */ -(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile -``` +**PluginConfig**: [Type Declaration](https://github.com/nonara/ts-patch/blob/master/projects/core/shared/plugin-types.ts) +**TransformerExtras**: [Type Declaration](https://github.com/nonara/ts-patch/blob/master/projects/core/shared/plugin-types.ts) +**ts.TransformerFactory**: `(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile` -#### compilerOptions -```ts -(compilerOpts: ts.CompilerOptions, config: PluginConfig) => ts.TransformerFactory -``` +_Note: Additional [legacy signatures](https://github.com/cevek/ttypescript#pluginconfigtype) are supported, but it is not recommended to develop a new transformer using them._ -## Usage -### Transforming AST Nodes +### Source Transformer Example Transformers can be written in JS or TS. ```ts -// transformer1-module -import * as ts from 'typescript'; -export default function(program: ts.Program, pluginOptions: any) { - return (ctx: ts.TransformationContext) => { - return (sourceFile: ts.SourceFile) => { - function visitor(node: ts.Node): ts.Node { - // if (ts.isCallExpression(node)) { - // return ts.createLiteral('call'); - // } - return ts.visitEachChild(node, visitor, ctx); - } - return ts.visitEachChild(sourceFile, visitor, ctx); - }; +import type * as ts from 'typescript'; +import type { TransformerExtras, PluginConfig } from 'ts-patch'; + +/** Changes string literal 'before' to 'after' */ +export default function (program: ts.Program, pluginConfig: PluginConfig, { ts: tsInstance }: TransformerExtras) { + return (ctx: ts.TransformationContext) => { + const { factory } = ctx; + + return (sourceFile: ts.SourceFile) => { + function visit(node: ts.Node): ts.Node { + if (tsInstance.isStringLiteral(node) && node.text === 'before') { + return factory.createStringLiteral('after'); + } + return tsInstance.visitEachChild(node, visit, ctx); + } + return tsInstance.visitNode(sourceFile, visit); }; + }; } ``` -#### Example Node Transformers +**Live Examples**: [`{ transform: "typescript-transform-paths" }`](https://github.com/LeDDGroup/typescript-transform-paths) [`{ transform: "typescript-is/lib/transform-inline/transformer" }`](https://github.com/woutervh-/typescript-is) -[`{ transform: "ts-transform-img/dist/transform", type: "config" }`](https://github.com/longlho/ts-transform-img) +[`{ transform: "typia/lib/transform" }`](https://github.com/samchon/typia) -[`{ transform: "ts-transform-css-modules/dist/transform", type: "config" }`](https://github.com/longlho/ts-transform-css-modules) +### Altering Diagnostics -[`{ transform: "ts-transform-react-intl/dist/transform", import: "transform", type: "config" }`](https://github.com/longlho/ts-transform-react-intl) +Diagnostics can be altered in a Source Transformer. -[`{ transform: "ts-nameof", type: "raw" }`](https://github.com/dsherret/ts-nameof) +To alter diagnostics you can use the following, provided from the `TransformerExtras` parameter: -[`{ transform: "typescript-transform-jsx" }`](https://github.com/LeDDGroup/typescript-transform-jsx) +| property | description | +|--------------------|-----------------------------------------------------| +| diagnostics | Reference to `Diagnostic` array | +| addDiagnostic() | Safely add `Diagnostic` to `diagnostics` array | +| removeDiagnostic() | Safely remove `Diagnostic` from `diagnostics` array | -[`{ transform: "ts-transformer-minify-privates" }`](https://github.com/timocov/ts-transformer-minify-privates) +### Note -[`{ transform: "typia/lib/transform" }`](https://github.com/samchon/typia) +_This alters diagnostics during _emit only_. If you want to alter diagnostics in your IDE as well, you'll need to create a LanguageService plugin to accompany your source transformer_ -### Transforming Program +## Program Transformers Sometimes you want to do more than just transform source code. For example you may want to: @@ -210,32 +219,30 @@ Sometimes you want to do more than just transform source code. For example you m For this, we've introduced what we call a Program Transformer. The transform action takes place during `ts.createProgram`, and allows re-creating the `Program` instance that typescript uses. -#### Configuring Program Transformer - -To configure a Program Transformer, supply `"transformProgram": true` in the config transformer entry. +### Program Transformer Entry Point -_Note: The `type`, `before`, and `after` options do not apply to a Program Transformer and will be ignored_ +```ts +(program: ts.Program, host: ts.CompilerHost | undefined, options: PluginConfig, extras: ProgramTransformerExtras) => ts.Program +``` -[See Config Example](#tsconfigjson) +**ProgramTransformerExtras** >>> [Type Declaration](https://github.com/nonara/ts-patch/blob/master/projects/core/shared/plugin-types.ts) -#### Signature +### Configuring Program Transformers -There is only one possible signature for a Program Transformer entry point. +To configure a Program Transformer, supply `"transformProgram": true` in the config transformer entry. -```TS -(program: ts.Program, host: ts.CompilerHost | undefined, options: PluginConfig, extras: ProgramTransformerExtras) => ts.Program -``` +_Note: The `before`, `after`, and `afterDeclarations` options do not apply to a Program Transformer and will be ignored_ -_ProgramTransformerExtras_ >>> [See Type Declaration](https://github.com/nonara/ts-patch/blob/master/src/installer/plugin-types.ts#L90) +[See Config Example](#configuration) -#### Example Program Transformer +### Example Program Transformer ```TypeScript /** * Add a file to Program */ -import * as ts from 'typescript'; import * as path from 'path'; -import { ProgramTransformerExtras, PluginConfig } from 'ts-patch'; +import type * as ts from 'typescript'; +import type { ProgramTransformerExtras, PluginConfig } from 'ts-patch'; export const newFile = path.resolve(__dirname, 'added-file.ts'); @@ -256,53 +263,59 @@ export default function ( **Note:** For a more complete example, see [Transforming Program with additional AST transformations](https://github.com/nonara/ts-patch/discussions/29#discussioncomment-325979) -### Altering Diagnostics - -Diagnostics can be altered in a Source Transformer. - -To alter diagnostics, use the [program type signature](#program-default), and use the following properties from the -`TransformerExtras` parameter - -| property | description | -| -------- |----------- | -| diagnostics | Reference to `Diagnostic` array -| addDiagnostic() | Directly add `Diagnostic` to `diagnostics` array | -| removeDiagnostic() | Directly remove `Diagnostic` from `diagnostics` array (uses splice, for safe removal) - -#### Notes -- This alters diagnostics during _emit only_. If you want to alter diagnostics in your IDE as well, you'll need to create a LanguageService plugin to accompany your source transformer - ## Resources ### Recommended Reading -- [Advice for working with the TS Compiler API](https://github.com/nonara/ts-patch/discussions/31) (**must read**) -- [TypeScript Transformer Handbook](https://github.com/madou/typescript-transformer-handbook) (**must read**) +- How-To: [Advice for working with the TS Compiler API](https://github.com/nonara/ts-patch/discussions/31) +- How-To: [TypeScript Transformer Handbook](https://github.com/madou/typescript-transformer-handbook) - Article: [How to Write a TypeScript Transform (Plugin)](https://dev.doctorevidence.com/how-to-write-a-typescript-transform-plugin-fc5308fdd943) - Article: [Creating a TypeScript Transformer](https://43081j.com/2018/08/creating-a-typescript-transform?source=post_page-----731e2b0b66e6----------------------) ### Recommended Tools -| Tool | Type | Description | -| ---- | ---- | ----------- | -| [TS AST Viewer](https://ts-ast-viewer.com/) | Website | Allows you to see the `Node` structure and other TS properties of your source code. -| [ts-query](https://www.npmjs.com/package/@phenomnomnominal/tsquery) | NPM Package | Perform fast CSS-like queries on AST to find specific nodes (by attribute, kind, name, etc) -| [ts-query Playground](https://tsquery-playground.firebaseapp.com/) | Website | Test `ts-query` in realtime -| [ts-expose-internals](https://github.com/nonara/ts-expose-internals) | NPM Package | Exposes internal types and methods of the TS compiler API +| Tool | Type | Description | +|----------------------------------------------------------------------|-------------|---------------------------------------------------------------------------------------------| +| [TS AST Viewer](https://ts-ast-viewer.com/) | Web App | Allows you to see the `Node` structure and other TS properties of your source code. | +| [ts-expose-internals](https://github.com/nonara/ts-expose-internals) | NPM Package | Exposes internal types and methods of the TS compiler API | + +### Discussion + +- `#compiler-internals-and-api` on [TypeScript Discord Server](https://discord.com/invite/typescript) +- TSP [Discussions](https://github.com/nonara/ts-patch/discussions) Board + +# Advanced Options + +**(env) `TSP_SKIP_CACHE`** + +Skips patch cache when patching via cli or live compiler. + +**(env) `TSP_COMPILER_TS_PATH`** + +Specify typescript library path to use for `ts-patch/compiler` (defaults to `require.resolve('typescript')`) + +**(env) `TSP_CACHE_DIR`** + +Override patch cache directory + +**(cli) `ts-patch clear-cache`** + +Cleans patch cache & lockfiles + +# Maintainers -## Credit -| Author | Module | -| --------------------- | ----------- | -| [Ron S.](https://twitter.com/Ron) | [ts-patch](https://github.com/nonara/ts-patch/) | -| [cevek](https://github.com/cevek) | [ttypescript](https://github.com/cevek/ttypescript) | + + + + + + +

Ron S.
-## HALP!!! +## Help Wanted -- Start here: [Recommended Reading](#recommended-reading) -- Ask on [StackOverflow](https://stackoverflow.com/questions/tagged/typescript-compiler-api) with the `#typescript-compiler-api` tag -- Read the handbook and still stuck? [Ask in Discussions](https://github.com/nonara/ts-patch/discussions) - someone may answer if they have time. -- Check out the `#compiler-api` room on the [TypeScript Discord Server](https://discord.com/invite/typescript). +If you're interested in helping and have a _high level_ of skill with the TS compiler API, please reach out! -## License +# License This project is licensed under the MIT License, as described in `LICENSE.md` diff --git a/jest.config.ts b/jest.config.ts index 75cf668..4776872 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,4 +1,5 @@ import type { Config } from '@jest/types'; +import * as os from 'os'; const config: Config.InitialOptions = { testEnvironment: "node", @@ -12,16 +13,16 @@ const config: Config.InitialOptions = { } }, modulePaths: [ "/node_modules" ], - coveragePathIgnorePatterns: [ - 'src/installer/lib/system/errors.ts$' - ], - globalSetup: './test/src/setup.js', - globalTeardown: './test/src/teardown.js', + // coveragePathIgnorePatterns: [ + // 'src/installer/lib/system/errors.ts$' + // ], + globalSetup: '/test/src/prepare.ts', + globalTeardown: '/test/src/cleanup.ts', testTimeout: 10000, transformIgnorePatterns: [ '/node_modules/(?!(ts-transformer-keys|ts-transformer-enumerate|ts-nameof)/)' ], - maxWorkers: 1 // Have to set this for now, as mockFs seems to mess up other threads + maxConcurrency: os.cpus().length } export default config; diff --git a/package.json b/package.json index e0dc0e1..5af87ae 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,23 @@ { "name": "ts-patch", - "version": "2.1.0", + "version": "3.0.0-rc4", "description": "Patch typescript to support custom transformers in tsconfig.json", "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { - "compile": "yarn run compile:installer && yarn run compile:patch", - "build": "yarn run clean && yarn run compile:patch && yarn run compile:installer", - "compile:installer": "tsc -p src", - "compile:patch": "tsc -p src/patch", + "compile": "yarn run compile:core && yarn run compile:patch", + "build": "yarn run clean && yarn run compile:patch && yarn run compile:core", + "compile:core": "tsc -p projects/core", + "compile:patch": "tsc -p projects/patch", "------------ ": "-------------", "clean": "npx rimraf dist coverage *.tsbuildinfo test/.tmp", "clean:global": "yarn run clean && npx rimraf ./**/node_modules ./**/yarn.lock", "reset": "yarn run clean:global && yarn install && yarn build", "------------ ": "-------------", "test": "jest", + "perf": "cd test && yarn run perf", "------------": "-------------", - "prepare": "ts-patch i -s && yarn prepare:patch && yarn prepare:test", - "prepare:patch": "cd src/patch && yarn install && tsc", + "prepare": "ts-patch install -s && yarn prepare:test", "prepare:test": "cd test && yarn install", "postbuild": "node scripts/postbuild.js" }, @@ -45,81 +45,43 @@ "homepage": "https://github.com/nonara/ts-patch#readme", "dependencies": { "chalk": "^4.1.2", - "glob": "^8.0.3", "global-prefix": "^3.0.0", - "minimist": "^1.2.6", - "resolve": "^1.22.1", - "shelljs": "^0.8.5", + "minimist": "^1.2.8", + "resolve": "^1.22.2", + "semver": "^7.3.8", "strip-ansi": "^6.0.1" }, "bin": { - "ts-patch": "./dist/bin/cli.js" - }, - "peerDependencies": { - "typescript": ">=4.0.0" + "ts-patch": "./dist/bin/ts-patch.js", + "tspc": "./dist/bin/tspc.js" }, "devDependencies": { - "@phenomnomnominal/tsquery": "^4.1.0", + "@types/esm": "^3.2.0", "@types/jest": "^28.1.6", "@types/minimist": "^1.2.2", "@types/mock-fs": "^4.13.1", "@types/node": "^16.11.5", "@types/resolve": "^1.20.1", + "@types/semver": "^7.3.13", "@types/shelljs": "^0.8.9", - "@types/ts-expose-internals": "npm:ts-expose-internals@4.4.4", - "graphql": "^15.4.0", - "graphql-tag": "^2.10.3", + "esm": "^3.2.25", + "glob": "^7.1.7", "jest": "^28.1.3", "jest-mock-process": "^1.4.1", - "mock-fs": "^5.1.1", - "rfdc": "^1.3.0", "rimraf": "^3.0.2", + "shelljs": "^0.8.5", "standard-version": "^9.3.2", "ts-jest": "^28.0.7", - "ts-node": "^10.4.0", - "ts-patch": "^2.0.2", - "ts-transform-img": "^0.4.2", - "ts-transform-react-intl": "^0.4.1", - "tsconfig-paths": "^3.11.0", - "typescript": "4.8.4" + "ts-node": "^10.9.1", + "ts-patch": "3.0.0-rc2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.0.4" }, "directories": { "resources": "./dist/resources" }, "standard-version": { "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "hidden": true - }, - { - "type": "docs", - "hidden": true - }, - { - "type": "style", - "hidden": true - }, - { - "type": "refactor", - "hidden": true - }, - { - "type": "perf", - "hidden": true - }, - { - "type": "test", - "hidden": true - }, { "type": "change", "section": "Changes" diff --git a/src/plugin.ts b/projects/core/plugin.ts similarity index 81% rename from src/plugin.ts rename to projects/core/plugin.ts index 966a299..354b48e 100644 --- a/src/plugin.ts +++ b/projects/core/plugin.ts @@ -22,8 +22,9 @@ export default function transformProgram( return ts.createProgram(program.getRootFileNames(), program.getCompilerOptions(), host, program); function hookWriteRootDirsFilenames() { - const sourceDir = program.getCommonSourceDirectory(); - const outputDir = program.getCompilerOptions().outDir; + // TODO - tsei + const sourceDir = (program).getCommonSourceDirectory(); + const outputDir = program.getCompilerOptions().outDir!; const originalWriteFile = host.writeFile; host.writeFile = (fileName: string, data: string, ...args: any[]) => { @@ -31,7 +32,8 @@ export default function transformProgram( for (const dir of rootDirs) { const relPath = path.relative(dir, srcPath); - if (relPath.slice(0, 2) !== '..') fileName = ts.normalizePath(path.resolve(outputDir, relPath)); + // TODO - tsei + if (relPath.slice(0, 2) !== '..') fileName = (ts).normalizePath(path.resolve(outputDir, relPath)); } return (originalWriteFile)(fileName, data, ...args); diff --git a/projects/core/resolver-hook.js b/projects/core/resolver-hook.js new file mode 100644 index 0000000..bfd3a76 --- /dev/null +++ b/projects/core/resolver-hook.js @@ -0,0 +1,55 @@ +const path = require('path'); +const Module = require('module'); + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +/** + * Enable rootDirs merge support for require (used with ts-node) + */ +function hookRequire() { + if (rootDirs.length > 0) { + const originalRequire = Module.prototype.require; + + Module.prototype.require = function (request) { + if (!path.isAbsolute(request) && request.startsWith('.')) { + const moduleDir = path.dirname(this.filename); + const moduleRootDir = rootDirs.find(rootDir => moduleDir.startsWith(rootDir)); + + if (moduleRootDir) { + const moduleRelativeFromRoot = path.relative(moduleRootDir, moduleDir); + + if (moduleRootDir) { + for (const rootDir of rootDirs) { + const possiblePath = path.join(rootDir, moduleRelativeFromRoot, request); + + let resolvedPath; + try { + resolvedPath = require.resolve(possiblePath); + } catch (e) { + continue; + } + + return originalRequire.call(this, resolvedPath); + } + } + } + } + + return originalRequire.call(this, request); + }; + } +} + +// endregion + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +const tsConfig = require(path.join(__dirname, 'tsconfig.json')); +const rootDirs = tsConfig.compilerOptions.rootDirs.map(rootDir => path.join(__dirname, rootDir)); + +hookRequire(); diff --git a/src/shared/plugin-types.ts b/projects/core/shared/plugin-types.ts similarity index 94% rename from src/shared/plugin-types.ts rename to projects/core/shared/plugin-types.ts index 769e347..4fd6f49 100644 --- a/src/shared/plugin-types.ts +++ b/projects/core/shared/plugin-types.ts @@ -1,3 +1,6 @@ +/** + * NOTE: This file is used during the build process for patch as well + */ import type ts from 'typescript'; @@ -18,6 +21,11 @@ export interface PluginConfig { */ transform?: string; + /** + * Resolve Path Aliases? + */ + resolvePathAliases?: boolean; + /** * tsconfig.json file (for transformer) */ @@ -28,6 +36,11 @@ export interface PluginConfig { */ import?: string; + /** + * Is the transformer an ES Module + */ + isEsm?: boolean + /** * Plugin entry point format type, default is program */ @@ -48,12 +61,6 @@ export interface PluginConfig { * not apply) Entry point must be (program: Program, host?: CompilerHost) => Program */ transformProgram?: boolean; - - /** - * Alias to transformProgram - * @deprecated - */ - beforeEmit?: boolean; } export type TransformerList = Required; diff --git a/projects/core/src/actions/check.ts b/projects/core/src/actions/check.ts new file mode 100644 index 0000000..0c42821 --- /dev/null +++ b/projects/core/src/actions/check.ts @@ -0,0 +1,70 @@ +import { LogLevel, PatchError } from '../system'; +import chalk from 'chalk'; +import { getTsPackage } from '../ts-package'; +import { PatchDetail } from "../patch/patch-detail"; +import { getTsModule } from "../module"; +import { getInstallerOptions, InstallerOptions } from "../options"; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +interface CheckResult { + [moduleName: string]: PatchDetail | undefined; +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +/** + * Check if files can be patched + */ +export function check(moduleName?: string | string[], opts?: Partial): CheckResult +export function check(moduleNames?: string[], opts?: Partial): CheckResult +export function check(moduleNameOrNames?: string | string[], opts?: Partial): CheckResult { + let targetModuleNames = moduleNameOrNames ? [ moduleNameOrNames ].flat() : undefined; + const options = getInstallerOptions(opts); + const { logger: log, dir } = options; + + /* Load Package */ + const tsPackage = getTsPackage(dir); + const { packageDir, version } = tsPackage; + + + targetModuleNames ??= tsPackage.moduleNames; + + /* Check Modules */ + log(`Checking TypeScript ${chalk.blueBright(`v${version}`)} installation in ${chalk.blueBright(packageDir)}\r\n`); + + let res: CheckResult = {}; + for (const moduleName of targetModuleNames) { + /* Validate */ + if (!tsPackage.moduleNames.includes(moduleName)) + throw new PatchError(`${moduleName} is not a valid TypeScript module in ${packageDir}`); + + /* Report */ + const tsModule = getTsModule(tsPackage, moduleName, { skipCache: options.skipCache }); + const { patchDetail } = tsModule.moduleFile; + + if (patchDetail !== undefined) { + const { isOutdated } = patchDetail; + log([ '+', + `${chalk.blueBright(moduleName)} is patched with ts-patch version ` + + `${chalk[isOutdated ? 'redBright' : 'blueBright'](patchDetail.tspVersion)} ${isOutdated ? '(out of date)' : ''}` + ]); + } else log([ '-', `${chalk.blueBright(moduleName)} is not patched.` ]); + + res[moduleName] = patchDetail; + + log('', LogLevel.verbose); + } + + return res; +} + +// endregion diff --git a/projects/core/src/actions/index.ts b/projects/core/src/actions/index.ts new file mode 100644 index 0000000..1880b7c --- /dev/null +++ b/projects/core/src/actions/index.ts @@ -0,0 +1,5 @@ +export * from './patch' +export * from './unpatch' +export * from './check' +export * from './install' +export * from './uninstall' diff --git a/projects/core/src/actions/install.ts b/projects/core/src/actions/install.ts new file mode 100644 index 0000000..5516e03 --- /dev/null +++ b/projects/core/src/actions/install.ts @@ -0,0 +1,23 @@ +import chalk from 'chalk'; +import { getInstallerOptions, InstallerOptions, patch } from '..'; +import { defaultInstallLibraries } from '../config'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +/** + * Patch TypeScript modules + */ +export function install(opts?: Partial) { + const options = getInstallerOptions(opts); + const { logger: log } = options; + + const ret = patch(defaultInstallLibraries, options); + if (ret) log([ '+', chalk.green(`ts-patch installed!`) ]); + + return ret; +} + +// endregion diff --git a/projects/core/src/actions/patch.ts b/projects/core/src/actions/patch.ts new file mode 100644 index 0000000..a5d1c76 --- /dev/null +++ b/projects/core/src/actions/patch.ts @@ -0,0 +1,98 @@ +import { LogLevel, PatchError, TspError, } from '../system'; +import { getTsPackage } from '../ts-package'; +import chalk from 'chalk'; +import { getModuleFile, getTsModule, ModuleFile } from '../module'; +import path from 'path'; +import { getInstallerOptions, InstallerOptions } from '../options'; +import { writeFileWithLock } from '../utils'; +import { getPatchedSource } from '../patch/get-patched-source'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +/** + * Patch a TypeScript module + */ +export function patch(moduleName: string, opts?: Partial): boolean +export function patch(moduleNames: string[], opts?: Partial): boolean +export function patch(moduleNameOrNames: string | string[], opts?: Partial): boolean { + const targetModuleNames = [ moduleNameOrNames ].flat(); + if (!targetModuleNames.length) throw new PatchError(`Must provide at least one module name to patch`); + + const options = getInstallerOptions(opts); + const { logger: log, dir, skipCache } = options; + + /* Load Package */ + const tsPackage = getTsPackage(dir); + + /* Get modules to patch and patch info */ + const moduleFiles: [ string, ModuleFile ][] = + targetModuleNames.map(m => [ m, getModuleFile(tsPackage.getModulePath(m)) ]); + + /* Determine files not already patched or outdated */ + const patchableFiles = moduleFiles.filter(entry => { + const [ moduleName, moduleFile ] = entry; + if (!moduleFile.patchDetail || moduleFile.patchDetail.isOutdated) return true; + else { + log([ '!', + `${chalk.blueBright(moduleName)} is already patched with the latest version. For details, run: ` + + chalk.bgBlackBright('ts-patch check') + ]); + + return false; + } + }); + + if (!patchableFiles.length) return true; + + /* Patch modules */ + const failedModulePaths: string[] = []; + for (let entry of patchableFiles) { + /* Load Module */ + const { 1: moduleFile } = entry; + const tsModule = getTsModule(tsPackage, moduleFile, { skipCache: true }); + + const { moduleName, modulePath } = tsModule; + log( + [ '~', `Patching ${chalk.blueBright(moduleName)} in ${chalk.blueBright(path.dirname(modulePath ))}` ], + LogLevel.verbose + ); + + try { + const { js, dts, loadedFromCache } = getPatchedSource(tsModule, { skipCache, log }); + + /* Write Patched Module */ + log( + [ + '~', + `Writing patched ${chalk.blueBright(moduleName)} to ` + + `${chalk.blueBright(modulePath)}${loadedFromCache ? ' (cached)' : ''}` + ], + LogLevel.verbose + ); + + writeFileWithLock(tsModule.modulePath, js!); + if (dts) writeFileWithLock(tsModule.dtsPath!, dts!); + + log([ '+', chalk.green(`Successfully patched ${chalk.bold.yellow(moduleName)}.\r\n`) ], LogLevel.verbose); + } catch (e) { + if (e instanceof TspError || options.logLevel >= LogLevel.verbose) log([ '!', e.message ]); + failedModulePaths.push(tsModule.modulePath); + } + } + + if (failedModulePaths.length > 1) { + log([ '!', + `Some files can't be patched! You can run again with --verbose to get specific error detail. The following files are unable to be ` + + `patched:\n - ${failedModulePaths.join('\n - ')}` + ]); + + return false; + } + + return true; +} + +// endregion diff --git a/projects/core/src/actions/uninstall.ts b/projects/core/src/actions/uninstall.ts new file mode 100644 index 0000000..d38e08e --- /dev/null +++ b/projects/core/src/actions/uninstall.ts @@ -0,0 +1,24 @@ +import chalk from 'chalk'; +import { defaultInstallLibraries } from '../config'; +import { unpatch } from './unpatch'; +import { getInstallerOptions, InstallerOptions } from "../options"; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +/** + * Remove patches from TypeScript modules + */ +export function uninstall(opts?: Partial) { + const options = getInstallerOptions(opts); + const { logger: log } = options; + + const ret = unpatch(defaultInstallLibraries, opts); + if (ret) log([ '-', chalk.green(`ts-patch removed!`) ]); + + return ret; +} + +// endregion diff --git a/projects/core/src/actions/unpatch.ts b/projects/core/src/actions/unpatch.ts new file mode 100644 index 0000000..c3eb9eb --- /dev/null +++ b/projects/core/src/actions/unpatch.ts @@ -0,0 +1,102 @@ +import { LogLevel, PatchError, RestoreError } from '../system'; +import chalk from 'chalk'; +import path from 'path'; +import { getTsPackage } from '../ts-package'; +import { getModuleFile, getTsModule, ModuleFile } from '../module'; +import fs from 'fs'; +import { getInstallerOptions, InstallerOptions } from '../options'; +import { copyFileWithLock } from '../utils'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function unpatch(moduleName: string, opts?: Partial): boolean +export function unpatch(moduleNames: string[], opts?: Partial): boolean +export function unpatch(moduleNameOrNames: string | string[], opts?: Partial): boolean { + let res = false; + + const targetModuleNames = [ moduleNameOrNames ].flat(); + if (!targetModuleNames.length) throw new PatchError(`Must provide at least one module name to patch`); + + const options = getInstallerOptions(opts); + const { logger: log, dir } = options; + + /* Load Package */ + const tsPackage = getTsPackage(dir); + + /* Get modules to patch and patch info */ + const moduleFiles: [ string, ModuleFile ][] = + targetModuleNames.map(m => [ m, getModuleFile(tsPackage.getModulePath(m)) ]); + + /* Determine patched files */ + const unpatchableFiles = moduleFiles.filter(entry => { + const [ moduleName, moduleFile ] = entry; + if (moduleFile.patchDetail) return true; + else { + log([ '!', `${chalk.blueBright(moduleName)} is not patched. For details, run: ` + chalk.bgBlackBright('ts-patch check') ]); + return false; + } + }); + + /* Restore files */ + const errors: Record = {}; + for (const entry of unpatchableFiles) { + /* Load Module */ + const { 1: moduleFile } = entry; + const tsModule = getTsModule(tsPackage, moduleFile, { skipCache: true }); + + try { + /* Get Backups */ + const backupPaths: string[] = [] + backupPaths.push(tsModule.backupCachePaths.js); + if (tsModule.backupCachePaths.dts) backupPaths.push(tsModule.backupCachePaths.dts); + + const baseNames = backupPaths.map(p => path.basename(p)).join(' & '); + + log( + [ + '~', + `Restoring ${chalk.blueBright(baseNames)} in ${chalk.blueBright(path.dirname(tsPackage.libDir))}` + ], + LogLevel.verbose + ); + + /* Restore files */ + for (const backupPath of backupPaths) { + if (!fs.existsSync(backupPath)) + throw new Error(`Cannot find backup file: ${backupPath}. Try reinstalling typescript.`); + + const moduleDir = path.dirname(tsModule.modulePath); + const destPath = path.join(moduleDir, path.basename(backupPath)); + + copyFileWithLock(backupPath, destPath); + } + + log([ '+', chalk.green(`Successfully restored ${chalk.bold.yellow(baseNames)}.\r\n`) ], LogLevel.verbose); + } catch (e) { + errors[tsModule.moduleName] = e; + } + } + + /* Handle errors */ + if (Object.keys(errors).length > 0) { + Object.values(errors).forEach(e => { + log([ '!', e.message ], LogLevel.verbose) + }); + + log(''); + throw new RestoreError( + `[${Object.keys(errors).join(', ')}]`, + 'Try reinstalling typescript.' + + (options.logLevel < LogLevel.verbose ? ' (Or, run uninstall again with --verbose for specific error detail)' : '') + ); + } else { + res = true; + } + + return res; +} + +// endregion diff --git a/projects/core/src/bin/ts-patch.ts b/projects/core/src/bin/ts-patch.ts new file mode 100644 index 0000000..19c58ae --- /dev/null +++ b/projects/core/src/bin/ts-patch.ts @@ -0,0 +1,7 @@ +import * as cliModule from '../cli/cli' + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +cliModule.run(); diff --git a/test/src/teardown.js b/projects/core/src/bin/tspc.ts similarity index 56% rename from test/src/teardown.js rename to projects/core/src/bin/tspc.ts index c371959..7a5a293 100644 --- a/test/src/teardown.js +++ b/projects/core/src/bin/tspc.ts @@ -1,11 +1,12 @@ -const fs = require('fs'); -const { tmpDir } = require('./config'); /* ****************************************************************************************************************** * - * Teardown + * Entry * ****************************************************************************************************************** */ -module.exports = () => { - if (fs.existsSync(tmpDir)) (fs.rmSync || fs.rmdirSync)(tmpDir, { recursive: true }); +// Run if main module cli +if (require.main === module) { + require('../compiler/tsc'); +} else { + throw new Error('tspc must be run as a CLI'); } diff --git a/projects/core/src/cli/cli.ts b/projects/core/src/cli/cli.ts new file mode 100644 index 0000000..e01c39a --- /dev/null +++ b/projects/core/src/cli/cli.ts @@ -0,0 +1,94 @@ +import minimist from 'minimist'; +import { createLogger, getCacheRoot, getLockFilePath, LogLevel } from '../system'; +import { getTsPackage } from '../ts-package'; +import chalk from 'chalk'; +import * as actions from '../actions'; +import { getCliOptions, getInstallerOptionsFromCliOptions } from './options'; +import { getCliCommand } from './commands'; +import { getHelpMenu } from './help-menu'; +import { tspPackageJSON } from '../config'; +import fs from 'fs'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export type CliConfig = Record + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function run(opt?: { cmdArgs?: string }) { + /* Parse Input */ + const args = minimist(opt?.cmdArgs?.split(' ') ?? process.argv.slice(2)); + const cliOptions = getCliOptions(args); + const cmd = getCliCommand(args); + + /* Setup */ + const options = getInstallerOptionsFromCliOptions(cliOptions); + const log = createLogger(options.logLevel, options.useColor, options.silent); + + try { + /* Handle commands */ + (() => { + switch (cmd) { + case 'help': + return log(getHelpMenu(), LogLevel.system); + + case 'version': + const { version: tsVersion, packageDir } = getTsPackage(options.dir); + return log('\r\n' + + chalk.bold.blue('ts-patch: ') + tspPackageJSON.version + '\r\n' + + chalk.bold.blue('typescript: ') + tsVersion + chalk.gray(` [${packageDir}]`), + LogLevel.system + ); + + case 'install': + return actions.install(options); + + case 'uninstall': + return actions.uninstall(options); + + case 'patch': + return actions.patch(args._.slice(1).join(' '), options); + + case 'unpatch': + return actions.unpatch(args._.slice(1).join(' '), options); + + case 'check': + return actions.check(undefined, options); + + case 'clear-cache': + const cacheRoot = getCacheRoot(); + + /* Clear dir */ + fs.rmSync(cacheRoot, { recursive: true, force: true }); + + /* Recreate Dirs */ + getCacheRoot(); + getLockFilePath(''); + + return log([ '+', 'Cleared cache & lock-files' ], LogLevel.system); + + default: + log([ '!', 'Invalid command. Try ts-patch /? for more info' ], LogLevel.system) + } + })(); + } + catch (e) { + log([ + '!', + chalk.bold.yellow(e.name && (e.name !== 'Error') ? `[${e.name}]: ` : 'Error: ') + chalk.red(e.message) + ], LogLevel.system); + } + + // Output for analysis by tests + return ({ cmd, args, options }); +} + +// endregion diff --git a/projects/core/src/cli/commands.ts b/projects/core/src/cli/commands.ts new file mode 100644 index 0000000..0b71aa3 --- /dev/null +++ b/projects/core/src/cli/commands.ts @@ -0,0 +1,55 @@ +import chalk from 'chalk'; +import minimist from 'minimist'; +import type { CliConfig } from './cli'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +/** @internal */ +export const cliCommandsConfig: CliConfig = { + install: { short: 'i', caption: `Installs ts-patch (to main libraries)` }, + uninstall: { short: 'u', caption: 'Restores original typescript files' }, + check: { + short: 'c', caption: + `Check patch status (use with ${chalk.cyanBright('--dir')} to specify TS package location)` + }, + patch: { + short: void 0, paramCaption: ' | ', caption: + 'Patch specific module(s) ' + chalk.yellow('(advanced)') + }, + unpatch: { + short: void 0, paramCaption: ' | ', caption: + 'Un-patch specific module(s) ' + chalk.yellow('(advanced)') + }, + 'clear-cache': { caption: 'Clears cache and lock-files' }, + version: { short: 'v', caption: 'Show version' }, + help: { short: '/?', caption: 'Show help menu' }, +}; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getCliCommand(args: minimist.ParsedArgs) { + let cmd: string | undefined = args._[0] ? args._[0].toLowerCase() : void 0; + + /* Handle special cases */ + if ((args.v) && (!cmd)) return 'version'; + if (args.h) return 'help'; + + if (!cmd) return cmd; + + /* Get long command */ + cmd = Object + .entries(cliCommandsConfig) + .find(([ long, { short } ]) => long === cmd || short === cmd)?.[0]; + + return cmd; +} + +// endregion diff --git a/projects/core/src/cli/help-menu.ts b/projects/core/src/cli/help-menu.ts new file mode 100644 index 0000000..726cc04 --- /dev/null +++ b/projects/core/src/cli/help-menu.ts @@ -0,0 +1,48 @@ +import chalk from 'chalk'; +import stripAnsi from 'strip-ansi'; +import { cliCommandsConfig } from './commands'; +import { cliOptionsConfig } from './options'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const LINE_INDENT = '\r\n\t'; +const COL_WIDTH = 45; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getHelpMenu() { + return LINE_INDENT + chalk.bold.blue('ts-patch [command] ') + chalk.blue('') + '\r\n' + LINE_INDENT + + + // Commands + Object + .entries(cliCommandsConfig) + .map(([ cmd, { short, caption, paramCaption } ]) => formatLine([ cmd, short ], caption, paramCaption)) + .join(LINE_INDENT) + + + // Options + '\r\n' + LINE_INDENT + chalk.bold('Options') + LINE_INDENT + + Object + .entries(cliOptionsConfig) + .map(([ long, { short, inverse, caption, paramCaption } ]) => formatLine([ + short && `${chalk.cyanBright('-' + short)}`, + long && `${chalk.cyanBright(`${inverse ? '--no-' : '--'}${long}`)}` + ], caption, paramCaption)) + .join(LINE_INDENT); + + function formatLine(left: (string | undefined)[], caption: string, paramCaption: string = '') { + const leftCol = left.filter(Boolean).join(chalk.blue(', ')) + ' ' + chalk.yellow(paramCaption); + const dots = chalk.grey('.'.repeat(COL_WIDTH - stripAnsi(leftCol).length)); + + return `${leftCol} ${dots} ${caption}`; + } +} + +// endregion diff --git a/projects/core/src/cli/options.ts b/projects/core/src/cli/options.ts new file mode 100644 index 0000000..626d854 --- /dev/null +++ b/projects/core/src/cli/options.ts @@ -0,0 +1,83 @@ +import minimist from 'minimist'; +import type { CliConfig } from './cli'; +import { LogLevel, OptionsError } from '../system'; +import { getInstallerOptions, InstallerOptions } from "../options"; +import { getGlobalTsDir } from "../utils"; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface CliOptions { + silent: boolean; + global: boolean; + verbose: boolean; + dir: string; + color: boolean; +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +/** @internal */ +export const cliOptionsConfig: CliConfig = { + silent: { short: 's', caption: 'Run silently' }, + global: { short: 'g', caption: 'Target global TypeScript installation' }, + verbose: { short: 'v', caption: 'Chat it up' }, + cache: { inverse: true, caption: 'Skip cache' }, + dir: { + short: 'd', + paramCaption: '', + caption: 'TypeScript directory or directory to resolve typescript package from' + }, + color: { inverse: true, caption: 'Strip ansi colours from output' } +}; + + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getCliOptions(args: minimist.ParsedArgs) { + let res: CliOptions = {}; + + for (const [ key, { short } ] of Object.entries(cliOptionsConfig)) { + if (args.hasOwnProperty(key) || (short && args.hasOwnProperty(short))) { + (res)[key] = args.hasOwnProperty(key) ? args[key] : args[short!]; + } + } + + return res; +} + +export function getInstallerOptionsFromCliOptions(cliOptions: CliOptions): InstallerOptions { + let partialOptions: Partial = {}; + + /* Dir option */ + if (cliOptions.global && cliOptions.dir) throw new OptionsError(`Cannot specify both --global and --dir`); + if ('dir' in cliOptions) partialOptions.dir = cliOptions.dir; + if ('global' in cliOptions) partialOptions.dir = getGlobalTsDir(); + + /* LogLevel option */ + if (cliOptions.silent && cliOptions.verbose) throw new OptionsError(`Cannot specify both --silent and --verbose`); + if (cliOptions.silent) { + partialOptions.logLevel = LogLevel.system; + partialOptions.silent = true; + } + else if (cliOptions.verbose) partialOptions.logLevel = LogLevel.verbose; + + /* Color option */ + if (cliOptions.color) partialOptions.useColor = cliOptions.color; + + return getInstallerOptions(partialOptions); +} + +// endregion diff --git a/projects/core/src/compiler/package.json b/projects/core/src/compiler/package.json new file mode 100644 index 0000000..d48b9bf --- /dev/null +++ b/projects/core/src/compiler/package.json @@ -0,0 +1,4 @@ +{ + "name": "typescript", + "main": "./typescript.js" +} diff --git a/projects/core/src/compiler/tsc.js b/projects/core/src/compiler/tsc.js new file mode 100644 index 0000000..7f818ed --- /dev/null +++ b/projects/core/src/compiler/tsc.js @@ -0,0 +1,19 @@ +const indexPath = '../'; +const path = require('path'); +const { getLiveModule } = require(indexPath); +const { runInThisContext } = require("vm"); + + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +const { js, tsModule } = getLiveModule('tsc.js'); + +const script = runInThisContext(` + (function (exports, require, module, __filename, __dirname) { + ${js} + }); + `); + +script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); diff --git a/projects/core/src/compiler/tsserver.js b/projects/core/src/compiler/tsserver.js new file mode 100644 index 0000000..fc4aff0 --- /dev/null +++ b/projects/core/src/compiler/tsserver.js @@ -0,0 +1,19 @@ +const indexPath = '../'; +const path = require('path'); +const { getLiveModule } = require(indexPath); +const { runInThisContext } = require("vm"); + + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +const { js, tsModule } = getLiveModule('tsserver.js'); + +const script = runInThisContext(` + (function (exports, require, module, __filename, __dirname) { + ${js} + }); + `); + +script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); diff --git a/projects/core/src/compiler/tsserverlibrary.js b/projects/core/src/compiler/tsserverlibrary.js new file mode 100644 index 0000000..551ada2 --- /dev/null +++ b/projects/core/src/compiler/tsserverlibrary.js @@ -0,0 +1,19 @@ +const indexPath = '../'; +const path = require('path'); +const { getLiveModule } = require(indexPath); +const { runInThisContext } = require("vm"); + + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +const { js, tsModule } = getLiveModule('tsserverlibrary.js'); + +const script = runInThisContext(` + (function (exports, require, module, __filename, __dirname) { + ${js} + }); + `); + +script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); diff --git a/projects/core/src/compiler/typescript.js b/projects/core/src/compiler/typescript.js new file mode 100644 index 0000000..2379a67 --- /dev/null +++ b/projects/core/src/compiler/typescript.js @@ -0,0 +1,19 @@ +const indexPath = '../'; +const path = require('path'); +const { getLiveModule } = require(indexPath); +const { runInThisContext } = require("vm"); + + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +const { js, tsModule } = getLiveModule('typescript.js'); + +const script = runInThisContext(` + (function (exports, require, module, __filename, __dirname) { + ${js} + }); + `); + +script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); diff --git a/projects/core/src/config.ts b/projects/core/src/config.ts new file mode 100644 index 0000000..131ab13 --- /dev/null +++ b/projects/core/src/config.ts @@ -0,0 +1,69 @@ +import path from 'path'; +import fs from "fs"; +import ts from 'typescript'; + + +/* ****************************************************************************************************************** */ +// region: Library Config +/* ****************************************************************************************************************** */ + +/** + * Root directory for ts-patch + */ +// TODO - This should be improved at some point +export const appRoot = (() => { + const moduleDir = __dirname; + + const chkFile = (pkgFile: string) => + (fs.existsSync(pkgFile) && (require(pkgFile).name === 'ts-patch')) ? path.dirname(pkgFile) : void 0; + + const res = chkFile(path.join(moduleDir, 'package.json')) || chkFile(path.join(moduleDir, '../../../package.json')); + + if (!res) throw new Error(`Error getting app root. No valid ts-patch package file found in ` + moduleDir); + + return res; +})(); + +/** + * Package json for ts-patch + */ +export const tspPackageJSON = require(path.resolve(appRoot, 'package.json')); + +export const RESOURCES_PATH = path.join(appRoot, tspPackageJSON.directories.resources); + +export const defaultNodePrinterOptions: ts.PrinterOptions = { + newLine: ts.NewLineKind.LineFeed, + removeComments: false +}; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Patch Config +/* ****************************************************************************************************************** */ + +export const defaultInstallLibraries = [ 'tsc.js', 'typescript.js' ]; + +export const corePatchName = ``; + +export const modulePatchFilePath = path.resolve(appRoot, tspPackageJSON.directories.resources, 'module-patch.js'); +export const dtsPatchFilePath = path.resolve(appRoot, tspPackageJSON.directories.resources, 'module-patch.d.ts'); + +// TODO - should do this in a better/dynamic way later +export const tsWrapperOpen = `var ts = (() => {`; +export const tsWrapperClose = `})();`; + +export const execTscCmd = 'execTsc'; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Cache Config +/* ****************************************************************************************************************** */ + +export const cachedFilePatchedPrefix = 'patched.'; +export const lockFileDir = 'locks'; + +// endregion diff --git a/projects/core/src/index.ts b/projects/core/src/index.ts new file mode 100644 index 0000000..9b5aec5 --- /dev/null +++ b/projects/core/src/index.ts @@ -0,0 +1,4 @@ +export { InstallerOptions, getInstallerOptions } from './options'; +export { install, uninstall, patch, check } from './actions' +export { getLiveModule } from './module' +export * from './plugin-types' diff --git a/projects/core/src/module/get-live-module.ts b/projects/core/src/module/get-live-module.ts new file mode 100644 index 0000000..4512370 --- /dev/null +++ b/projects/core/src/module/get-live-module.ts @@ -0,0 +1,25 @@ +import path from 'path'; +import { getTsModule, TsModule } from './ts-module'; +import { getTsPackage } from '../ts-package'; +import { getPatchedSource } from '../patch/get-patched-source'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getLiveModule(moduleName: TsModule.Name) { + const skipCache = process.env.TSP_SKIP_CACHE === 'true'; + const tsPath = process.env.TSP_COMPILER_TS_PATH ? path.resolve(process.env.TSP_COMPILER_TS_PATH) : require.resolve('typescript'); + + /* Open the TypeScript module */ + const tsPackage = getTsPackage(tsPath); + const tsModule = getTsModule(tsPackage, moduleName, { skipCache }); + + /* Get patched version */ + const { js } = getPatchedSource(tsModule, { skipCache, skipDts: true }); + + return { js, tsModule }; +} + +// endregion diff --git a/projects/core/src/module/index.ts b/projects/core/src/module/index.ts new file mode 100644 index 0000000..8645efc --- /dev/null +++ b/projects/core/src/module/index.ts @@ -0,0 +1,4 @@ +export * from './ts-module'; +export * from './module-source'; +export * from './module-file'; +export * from './get-live-module'; diff --git a/projects/core/src/module/module-file.ts b/projects/core/src/module/module-file.ts new file mode 100644 index 0000000..b5ce043 --- /dev/null +++ b/projects/core/src/module/module-file.ts @@ -0,0 +1,121 @@ +import fs from 'fs'; +import { PatchDetail } from '../patch/patch-detail'; +import path from 'path'; +import { getHash, withFileLock } from '../utils'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const SHORT_CHUNK_SIZE = 1024; +const LONG_CHUNK_SIZE = 64_536; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface ModuleFile { + moduleName: string + patchDetail?: PatchDetail + filePath: string + get content(): string + + getHash(): string +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +function readFile(filePath: string, headersOnly?: boolean) { + return withFileLock(filePath, () => { + let CHUNK_SIZE = headersOnly ? SHORT_CHUNK_SIZE : LONG_CHUNK_SIZE; + let result = ''; + let doneReadingHeaders = false; + let bytesRead; + let buffer = Buffer.alloc(CHUNK_SIZE); + const headerLines: string[] = []; + let isLastHeaderIncomplete = false; + + const fd = fs.openSync(filePath, 'r'); + + try { + readFileLoop: + while ((bytesRead = fs.readSync(fd, buffer, 0, CHUNK_SIZE, null)) > 0) { + const chunkString = buffer.toString('utf-8', 0, bytesRead); + + /* Handle Header */ + if (!doneReadingHeaders) { + const lines = chunkString.split('\n'); + + lineLoop: + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (i === 0 && isLastHeaderIncomplete) { + headerLines[headerLines.length - 1] += line; + } else { + if (line.startsWith('///')) { + headerLines.push(line); + } else { + doneReadingHeaders = true; + if (!headersOnly) { + result += lines.slice(i).join('\n'); + CHUNK_SIZE = LONG_CHUNK_SIZE; + buffer = Buffer.alloc(CHUNK_SIZE); + break lineLoop; + } else { + break readFileLoop; + } + } + } + } + + if (!doneReadingHeaders) isLastHeaderIncomplete = !chunkString.endsWith('\n'); + } else { + /* Handle content */ + result += chunkString; + } + } + + return { headerLines, content: headersOnly ? undefined : result }; + } finally { + fs.closeSync(fd); + } + }); +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getModuleFile(filePath: string, loadFullContent?: boolean): ModuleFile { + let { headerLines, content } = readFile(filePath, !loadFullContent); + + /* Get PatchDetail */ + const patchDetail = PatchDetail.fromHeader(headerLines); + + return { + moduleName: path.basename(filePath), + filePath, + patchDetail, + get content() { + if (content == null) content = readFile(this.filePath, false).content; + return content!; + }, + getHash(): string { + return getHash(this.content); + } + }; +} + +// endregion diff --git a/projects/core/src/module/module-source.ts b/projects/core/src/module/module-source.ts new file mode 100644 index 0000000..c8ad1a7 --- /dev/null +++ b/projects/core/src/module/module-source.ts @@ -0,0 +1,54 @@ +import { createSourceSection, SourceSection } from './source-section'; +import { TsModule } from './ts-module'; +import { sliceModule } from '../slice/module-slice'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface ModuleSource { + fileHeader: SourceSection; + bodyHeader?: SourceSection; + body: SourceSection[]; + fileFooter?: SourceSection; + usesTsNamespace: boolean; + getSections(): [ sectionName: SourceSection['sectionName'], section: SourceSection | undefined ][]; +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getModuleSource(tsModule: TsModule): ModuleSource { + const moduleFile = tsModule.getUnpatchedModuleFile(); + + const { firstSourceFileStart, fileEnd, wrapperPos, bodyPos, sourceFileStarts } = + sliceModule(moduleFile, tsModule.package.version); + + const fileHeaderEnd = wrapperPos?.start ?? firstSourceFileStart; + + return { + fileHeader: createSourceSection(moduleFile, 'file-header', 0, fileHeaderEnd), + bodyHeader: wrapperPos && createSourceSection(moduleFile, 'body-header', bodyPos.start, firstSourceFileStart, 2), + body: sourceFileStarts.map(([ srcFileName, startPos ], i) => { + const endPos = sourceFileStarts[i + 1]?.[1] ?? bodyPos?.end ?? fileEnd; + return createSourceSection(moduleFile, 'body', startPos, endPos, wrapperPos != null ? 2 :0, srcFileName); + }), + fileFooter: wrapperPos && createSourceSection(moduleFile, 'file-footer', wrapperPos.end, fileEnd), + usesTsNamespace: wrapperPos != null, + getSections() { + return [ + [ 'file-header', this.fileHeader ], + [ 'body-header', this.bodyHeader ], + ...this.body.map((section, i) => [ `body`, section ] as [ SourceSection['sectionName'], SourceSection ]), + [ 'file-footer', this.fileFooter ], + ]; + } + } +} + +// endregion diff --git a/projects/core/src/module/source-section.ts b/projects/core/src/module/source-section.ts new file mode 100644 index 0000000..73e310f --- /dev/null +++ b/projects/core/src/module/source-section.ts @@ -0,0 +1,110 @@ +import ts from 'typescript'; +import { ModuleFile } from './module-file'; +import path from 'path'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface SourceSection { + readonly sectionName: 'file-header' | 'body-header' | 'body' | 'file-footer'; + readonly srcFileName?: string; + readonly pos: { start: number; end: number }; + indentLevel: number; + + hasTransformed?: boolean; + hasUpdatedSourceText?: boolean; + + get sourceText(): string; + updateSourceText(newText: string): void; + getSourceFile(): ts.SourceFile; + getOriginalSourceFile(): ts.SourceFile; + transform(transformers: ts.TransformerFactory[]): void; + print(printer?: ts.Printer): string; +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function createSourceSection( + moduleFile: ModuleFile, + sectionName: SourceSection['sectionName'], + startPos: number, + endPos: number, + indentLevel: number = 0, + srcFileName?: string, +): + SourceSection +{ + let sourceText: string | undefined; + let originalSourceFile: ts.SourceFile | undefined; + let sourceFile: ts.SourceFile | undefined; + let sourceFileName: string | undefined; + + return { + hasTransformed: false, + hasUpdatedSourceText: false, + sectionName, + srcFileName, + indentLevel, + pos: { start: startPos, end: endPos }, + get sourceText() { + return sourceText ??= moduleFile.content.slice(startPos, endPos); + }, + getSourceFile() { + if (!sourceFile) { + if (this.hasUpdatedSourceText) return createSourceFile(this); + else return this.getOriginalSourceFile(); + } + return sourceFile; + }, + updateSourceText(newText: string) { + sourceText = newText; + sourceFile = undefined; + }, + getOriginalSourceFile() { + originalSourceFile ??= createSourceFile(this); + return originalSourceFile; + }, + transform(transformers: ts.TransformerFactory[]) { + const result = ts.transform(this.getSourceFile(), transformers); + sourceFile = result.transformed[0]; + this.hasTransformed = true; + this.indentLevel = 0; + }, + print(printer?: ts.Printer) { + if (!this.hasTransformed) return this.sourceText; + + printer ??= ts.createPrinter(); + return printer.printFile(this.getSourceFile()); + } + } + + function createSourceFile(sourceSection: SourceSection) { + return ts.createSourceFile( + getSourceFileName(), + sourceSection.sourceText, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + } + + function getSourceFileName() { + if (!sourceFileName) { + sourceFileName = srcFileName; + if (!sourceFileName) { + const baseName = path.basename(moduleFile.filePath, path.extname(moduleFile.filePath)); + sourceFileName = `${baseName}.${sectionName}.ts`; + } + } + return sourceFileName; + } +} + +// endregion diff --git a/projects/core/src/module/ts-module.ts b/projects/core/src/module/ts-module.ts new file mode 100644 index 0000000..b07a561 --- /dev/null +++ b/projects/core/src/module/ts-module.ts @@ -0,0 +1,139 @@ +import path from 'path'; +import fs from 'fs'; +import type { TsPackage } from '../ts-package'; +import { getModuleSource, ModuleSource } from './module-source'; +import { getCachePath } from '../system'; +import { getModuleFile, ModuleFile } from './module-file'; +import { cachedFilePatchedPrefix } from '../config'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +export namespace TsModule { + export const names = ['tsc.js', 'tsserverlibrary.js', 'typescript.js', 'tsserver.js']; +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface TsModule { + package: TsPackage; + majorVer: number; + minorVer: number; + isPatched: boolean; + + moduleName: TsModule.Name; + modulePath: string; + moduleFile: ModuleFile; + dtsPath: string | undefined; + + cacheKey: string; + backupCachePaths: { js: string, dts?: string }; + patchedCachePaths: { js: string, dts?: string }; + + getUnpatchedModuleFile(): ModuleFile; + getUnpatchedSource(): ModuleSource; +} + +export namespace TsModule { + export type Name = (typeof names)[number] | string; +} + +export interface GetTsModuleOptions { + skipCache?: boolean +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getTsModule(tsPackage: TsPackage, moduleName: TsModule.Name, options?: GetTsModuleOptions): + TsModule +export function getTsModule(tsPackage: TsPackage, moduleFile: ModuleFile, options?: GetTsModuleOptions): TsModule +export function getTsModule( + tsPackage: TsPackage, + moduleNameOrModuleFile: TsModule.Name | ModuleFile, + options?: GetTsModuleOptions +): TsModule { + const skipCache = options?.skipCache; + + /* Get Module File */ + let moduleFile: ModuleFile | undefined; + let moduleName: string | undefined; + let modulePath: string | undefined; + if (typeof moduleNameOrModuleFile === "object" && moduleNameOrModuleFile.content) { + moduleFile = moduleNameOrModuleFile; + moduleName = moduleFile.moduleName; + modulePath = moduleFile.filePath; + } else { + moduleName = moduleNameOrModuleFile as TsModule.Name; + } + + /* Handle Local Cache */ + if (!skipCache && tsPackage.moduleCache.has(moduleName)) return tsPackage.moduleCache.get(moduleName)!; + + /* Load File (if not already) */ + if (!modulePath) modulePath = path.join(tsPackage.libDir, moduleName); + if (!moduleFile) moduleFile = getModuleFile(modulePath); + + /* Get DTS if exists */ + const maybeDtsFile = modulePath.replace(/\.js$/, '.d.ts'); + const dtsPath = fs.existsSync(maybeDtsFile) ? maybeDtsFile : undefined; + const dtsName = dtsPath && path.basename(dtsPath); + + /* Get Cache Paths */ + const cacheKey = moduleFile.patchDetail?.originalHash || moduleFile.getHash(); + const backupCachePaths = { + js: getCachePath(cacheKey, moduleName), + dts: dtsName && getCachePath(cacheKey, dtsName) + } + const patchedCachePaths = { + js: getCachePath(cacheKey, cachedFilePatchedPrefix + moduleName), + dts: dtsName && getCachePath(cacheKey, cachedFilePatchedPrefix + dtsName) + } + + /* Create Module */ + const isPatched = !!moduleFile.patchDetail; + let moduleSource: ModuleSource | undefined; + let originalModuleFile: ModuleFile | undefined; + const tsModule: TsModule = { + package: tsPackage, + majorVer: tsPackage.majorVer, + minorVer: tsPackage.minorVer, + isPatched, + + moduleName, + modulePath, + moduleFile, + dtsPath, + + cacheKey, + backupCachePaths, + patchedCachePaths, + + getUnpatchedSource() { + return getModuleSource(this); + }, + + getUnpatchedModuleFile() { + if (!originalModuleFile) originalModuleFile = isPatched ? getModuleFile(backupCachePaths.js) : moduleFile!; + + return originalModuleFile; + } + }; + + tsPackage.moduleCache.set(moduleName, tsModule); + + return tsModule; +} + +// endregion diff --git a/projects/core/src/options.ts b/projects/core/src/options.ts new file mode 100644 index 0000000..88e119a --- /dev/null +++ b/projects/core/src/options.ts @@ -0,0 +1,55 @@ +import { createLogger, Logger, LogLevel } from './system'; +import { PartialSome } from "./utils"; + + +/* ****************************************************************************************************************** */ +// region: Locals +/* ****************************************************************************************************************** */ + +type PreAppOptions = PartialSome + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +export interface InstallerOptions { + logLevel: LogLevel; + useColor: boolean; + dir: string; + silent: boolean; + logger: Logger; + skipCache: boolean; +} + +export namespace InstallerOptions { + export function getDefaults() { + return { + logLevel: LogLevel.normal, + useColor: true, + dir: process.cwd(), + silent: false, + skipCache: false + } satisfies PreAppOptions + } +} + +// endregion + + +/* ******************************************************************************************************************** + * Parser + * ********************************************************************************************************************/ + +export function getInstallerOptions(options?: Partial): InstallerOptions { + if (!options && typeof options === "object" && Object.isSealed(options)) return options as InstallerOptions; + + const res = { ...InstallerOptions.getDefaults(), ...options } as PreAppOptions; + + return Object.seal({ + ...res, + logger: res.logger ?? createLogger(res.logLevel, res.useColor, res.silent) + }); +} diff --git a/projects/core/src/patch/get-patched-source.ts b/projects/core/src/patch/get-patched-source.ts new file mode 100644 index 0000000..092f3e4 --- /dev/null +++ b/projects/core/src/patch/get-patched-source.ts @@ -0,0 +1,85 @@ +import { Logger, LogLevel } from '../system'; +import chalk from 'chalk'; +import path from 'path'; +import { copyFileWithLock, mkdirIfNotExist, readFileWithLock, writeFileWithLock } from '../utils'; +import fs from 'fs'; +import { getModuleFile, TsModule } from '../module'; +import { patchModule } from './patch-module'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface GetPatchedSourceOptions { + log?: Logger + skipCache?: boolean + skipDts?: boolean +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getPatchedSource(tsModule: TsModule, options?: GetPatchedSourceOptions): + { js: string, dts: string | undefined, loadedFromCache: boolean } +{ + const { backupCachePaths, patchedCachePaths } = tsModule; + const { log, skipCache } = options || {}; + + /* Write backup if not patched */ + if (!tsModule.isPatched) { + for (const [ key, backupPath ] of Object.entries(backupCachePaths)) { + const srcPath = key === 'dts' ? tsModule.dtsPath : tsModule.modulePath; + if (key === 'dts' && options?.skipDts) continue; + if (!srcPath) continue; + + log?.([ '~', `Writing backup cache to ${chalk.blueBright(backupPath)}` ], LogLevel.verbose); + + const cacheDir = path.dirname(backupPath); + mkdirIfNotExist(cacheDir); + copyFileWithLock(srcPath, backupPath); + } + } + + /* Get Patched Module */ + const canUseCache = !skipCache + && !tsModule.moduleFile.patchDetail?.isOutdated + && (!patchedCachePaths.dts || fs.existsSync(patchedCachePaths.dts)) + && fs.existsSync(patchedCachePaths.js) + && !getModuleFile(patchedCachePaths.js).patchDetail?.isOutdated; + + let js: string | undefined; + let dts: string | undefined; + if (canUseCache) { + js = readFileWithLock(patchedCachePaths.js); + dts = !options?.skipDts && patchedCachePaths.dts ? readFileWithLock(patchedCachePaths.dts) : undefined; + } else { + const res = patchModule(tsModule, options?.skipDts); + js = res.js; + dts = res.dts; + + /* Write patched cache */ + if (!skipCache) { + const cacheDir = path.dirname(patchedCachePaths.js); + + for (const [ key, patchPath ] of Object.entries(patchedCachePaths)) { + const srcPath = key === 'dts' ? dts : js; + if (key === 'dts' && options?.skipDts) continue; + if (!srcPath) continue; + + log?.([ '~', `Writing patched cache to ${chalk.blueBright(patchPath)}` ], LogLevel.verbose); + + mkdirIfNotExist(cacheDir); + writeFileWithLock(patchPath, srcPath); + } + } + } + + return { js, dts, loadedFromCache: canUseCache }; +} + +// endregion diff --git a/projects/core/src/patch/patch-detail.ts b/projects/core/src/patch/patch-detail.ts new file mode 100644 index 0000000..fd135cd --- /dev/null +++ b/projects/core/src/patch/patch-detail.ts @@ -0,0 +1,106 @@ +import { TsModule } from '../module'; +import { corePatchName, tspPackageJSON } from '../config'; +import semver from 'semver'; +import { getHash } from '../utils'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +export const tspHeaderBlockStart = '/// tsp-module:'; +export const tspHeaderBlockStop = '/// :tsp-module'; +export const currentPatchDetailVersion = '0.1.0'; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface PatchDetail { + tsVersion: string + tspVersion: string + patchDetailVersion: string + moduleName: TsModule.Name + originalHash: string + hash: string + patches: PatchDetail.PatchEntry[] +} + +export namespace PatchDetail { + export interface PatchEntry { + library: string + version: string + patchName?: string + hash?: string + blocksCache?: boolean + } +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: PatchDetail (class) +/* ****************************************************************************************************************** */ + +export class PatchDetail { + + /* ********************************************************* */ + // region: Methods + /* ********************************************************* */ + + get isOutdated() { + const packageVersion = tspPackageJSON.version; + return semver.gt(packageVersion, this.tspVersion); + } + + toHeader() { + const lines = JSON.stringify(this, null, 2) + .split('\n') + .map(line => `/// ${line}`) + .join('\n'); + + return `${tspHeaderBlockStart}\n${lines}\n${tspHeaderBlockStop}`; + } + + static fromHeader(header: string | string[]) { + const headerLines = Array.isArray(header) ? header : header.split('\n'); + + let patchDetail: PatchDetail | undefined; + const startIdx = headerLines.findIndex(line => line === tspHeaderBlockStart) + 1; + let endIdx = headerLines.findIndex(line => line === tspHeaderBlockStop); + if (endIdx === -1) headerLines.length; + if (startIdx && endIdx) { + const patchInfoStr = headerLines + .slice(startIdx, endIdx) + .map(line => line.replace('/// ', '')) + .join('\n'); + patchDetail = Object.assign(new PatchDetail(), JSON.parse(patchInfoStr) as PatchDetail); + } + + return patchDetail; + } + + static fromModule(tsModule: TsModule, patchedContent: string, patches: PatchDetail.PatchEntry[] = []) { + patches.unshift({ library: 'ts-patch', patchName: corePatchName, version: tspPackageJSON.version }); + + const patchDetail = { + tsVersion: tsModule.package.version, + tspVersion: tspPackageJSON.version, + patchDetailVersion: currentPatchDetailVersion, + moduleName: tsModule.moduleName, + originalHash: tsModule.cacheKey, + hash: getHash(patchedContent), + patches: patches + } satisfies Omit + + return Object.assign(new PatchDetail(), patchDetail); + } + + // endregion +} + +// endregion diff --git a/projects/core/src/patch/patch-module.ts b/projects/core/src/patch/patch-module.ts new file mode 100644 index 0000000..7d16890 --- /dev/null +++ b/projects/core/src/patch/patch-module.ts @@ -0,0 +1,197 @@ +import ts from 'typescript'; +import fs from 'fs'; +import { + defaultNodePrinterOptions, dtsPatchFilePath, execTscCmd, modulePatchFilePath, tsWrapperClose, tsWrapperOpen +} from '../config'; +import { getTsModule, TsModule } from '../module'; +import { + addOriginalCreateProgramTransformer, createMergeStatementsTransformer, fixTsEarlyReturnTransformer, + hookTscExecTransformer, patchCreateProgramTransformer, patchEmitterTransformer +} from './transformers'; +import { SourceSection } from '../module/source-section'; +import { PatchError } from '../system'; +import { readFileWithLock } from '../utils'; +import { PatchDetail } from './patch-detail'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const dtsPatchSrc = '\n' + fs.readFileSync(dtsPatchFilePath, 'utf-8'); +const jsPatchSrc = fs.readFileSync(modulePatchFilePath, 'utf-8') + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function patchModule(tsModule: TsModule, skipDts: boolean = false): { js: string, dts?: string } { + let shouldWrap: boolean = false; + switch (tsModule.moduleName) { + case 'tsc.js': + case 'tsserver.js': + case 'tsserverlibrary.js': + case 'typescript.js': + shouldWrap = true; + } + + const source = tsModule.getUnpatchedSource(); + + const printableBodyFooters: (SourceSection | string)[] = []; + const printableFooters: (SourceSection | string)[] = []; + + /* Splice in full compiler functionality (if not already present) */ + if (tsModule.moduleName !== 'typescript.js') { + const typescriptModule = getTsModule(tsModule.package, 'typescript.js'); + const tsSource = typescriptModule.getUnpatchedSource(); + + /* Merge Headers & Footer */ + mergeStatements(source.fileHeader, tsSource.fileHeader); + source.bodyHeader = tsSource.bodyHeader; + mergeStatements(source.fileFooter, tsSource.fileFooter); + + /* Replace body */ + for (let i = source.body.length - 1; i >= 0; i--) { + const bodySection = source.body[i]; + if (tsSource.body.some(s => s.srcFileName === bodySection.srcFileName)) { + source.body.splice(i, 1); + } + } + + source.body.unshift(...tsSource.body); + + /* Fix early return */ + const typescriptSection = source.body.find(s => s.srcFileName === 'src/typescript/typescript.ts'); + if (!typescriptSection) throw new PatchError(`Could not find Typescript source section`); + typescriptSection.transform([ fixTsEarlyReturnTransformer ]); + + printableBodyFooters.push(`return returnResult;`); + } + + /* Patch Program */ + const programSection = source.body.find(s => s.srcFileName === 'src/compiler/program.ts'); + if (!programSection) throw new PatchError(`Could not find Program source section`); + programSection.transform([ patchCreateProgramTransformer ]); + + /* Add originalCreateProgram to exports */ + const namespacesTsSection = source.body.find(s => s.srcFileName === 'src/typescript/_namespaces/ts.ts'); + if (!namespacesTsSection) throw new PatchError(`Could not find NamespacesTs source section`); + namespacesTsSection.transform([ addOriginalCreateProgramTransformer ]); + + /* Patch emitter (for diagnostics tools) */ + const emitterSection = source.body.find(s => s.srcFileName === 'src/compiler/watch.ts'); + if (!emitterSection) throw new PatchError(`Could not find Emitter source section`); + emitterSection.transform([ patchEmitterTransformer ]); + + /* Move executeCommandLine outside of closure */ + if (tsModule.moduleName === 'tsc.js') { + const tscSection = source.body.find(s => s.srcFileName === 'src/tsc/tsc.ts'); + if (!tscSection) throw new PatchError(`Could not find Tsc source section`); + + tscSection.transform([ hookTscExecTransformer ]); + + printableFooters.push(`tsp.${execTscCmd}();`); + } + + /* Print the module */ + const printedJs = printModule(); + + /* Get Dts */ + let dts: string | undefined; + if (!skipDts && tsModule.dtsPath) { + const dtsText = readFileWithLock(tsModule.dtsPath); + dts = + dtsPatchSrc + '\n' + + dtsText; + } + + /* Get JS */ + const patchDetail = PatchDetail.fromModule(tsModule, printedJs); + const js = + patchDetail.toHeader() + '\n' + + jsPatchSrc + '\n' + + printedJs; + + return { dts, js }; + + function getPrintList() { + const list: [item: (string | SourceSection | undefined), indent?: number][] = []; + let indentLevel = 0; + + /* File Header */ + list.push([ source.fileHeader, indentLevel ]); + + /* Body Wrapper Open */ + if (shouldWrap) { + list.push([ `\n${tsWrapperOpen}\n`, indentLevel ]); + indentLevel = 2; + } + + /* Body Header*/ + list.push([ source.bodyHeader, indentLevel ]); + + /* Body */ + source.body.forEach(section => list.push([ section, indentLevel ])); + + /* Body Footers */ + printableBodyFooters.forEach(f => list.push([ f, indentLevel ])); + + /* Body Wrapper Close */ + if (shouldWrap) { + indentLevel = 0; + list.push([ `\n${tsWrapperClose}\n`, indentLevel ]); + } + + /* File Footer */ + list.push([ source.fileFooter, indentLevel ]); + printableFooters.forEach(f => list.push([ f, indentLevel ])); + + return list; + } + + function printModule() { + const printer = ts.createPrinter(defaultNodePrinterOptions); + let outputStr = ``; + + for (const [ item, indentLevel ] of getPrintList()) { + let printed: string; + let addedIndent: number | undefined; + if (item === undefined) continue; + if (typeof item === 'string') { + printed = item; + } else { + printed = item.print(printer); + if (indentLevel && item.indentLevel < indentLevel) { + addedIndent = indentLevel - item.indentLevel; + } + } + + if (addedIndent) printed = printed.replace(/^/gm, ' '.repeat(addedIndent)); + + outputStr += printed; + } + + return outputStr; + } + + function mergeStatements( + baseSection: SourceSection | undefined, + addedSection: SourceSection | undefined, + ) { + if (!baseSection || !addedSection) { + if (addedSection) baseSection = addedSection; + return; + } + + const baseSourceFile = baseSection.getSourceFile(); + const addedSourceFile = addedSection.getSourceFile(); + + const transformer = createMergeStatementsTransformer(baseSourceFile, addedSourceFile); + baseSection.transform([ transformer ]); + } +} + +// endregion diff --git a/projects/core/src/patch/transformers/add-original-create-program.ts b/projects/core/src/patch/transformers/add-original-create-program.ts new file mode 100644 index 0000000..f9ebabd --- /dev/null +++ b/projects/core/src/patch/transformers/add-original-create-program.ts @@ -0,0 +1,68 @@ +import ts from 'typescript'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function addOriginalCreateProgramTransformer(context: ts.TransformationContext) { + const { factory } = context; + + let patchSuccess = false; + + return (sourceFile: ts.SourceFile) => { + if (sourceFile.fileName !== 'src/typescript/_namespaces/ts.ts') + throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); + + const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNodes) as unknown as ts.Statement[]); + + if (!patchSuccess) throw new Error('Failed to patch typescript early return!'); + + return res; + + function visitNodes(node: ts.Statement): ts.VisitResult { + if ( + ts.isExpressionStatement(node) && + ts.isCallExpression(node.expression) && + node.expression.expression.getText() === "__export" + ) { + const exportObjectLiteral = node.expression.arguments[1]; + if (ts.isObjectLiteralExpression(exportObjectLiteral)) { + const originalCreateProgramProperty = factory.createPropertyAssignment( + "originalCreateProgram", + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createIdentifier("originalCreateProgram") + ) + ); + + const updatedExportObjectLiteral = factory.updateObjectLiteralExpression( + exportObjectLiteral, + [...exportObjectLiteral.properties, originalCreateProgramProperty] + ); + + const updatedNode = factory.updateExpressionStatement( + node, + factory.updateCallExpression( + node.expression, + node.expression.expression, + undefined, + [node.expression.arguments[0], updatedExportObjectLiteral] + ) + ); + + patchSuccess = true; + return updatedNode; + } + } + + return node; + } + }; +} + +// endregion diff --git a/projects/core/src/patch/transformers/fix-ts-early-return.ts b/projects/core/src/patch/transformers/fix-ts-early-return.ts new file mode 100644 index 0000000..8f0e4d6 --- /dev/null +++ b/projects/core/src/patch/transformers/fix-ts-early-return.ts @@ -0,0 +1,46 @@ +import ts, { isReturnStatement } from 'typescript'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function fixTsEarlyReturnTransformer(context: ts.TransformationContext) { + const { factory } = context; + + let patchSuccess = false; + + return (sourceFile: ts.SourceFile) => { + if (sourceFile.fileName !== 'src/typescript/typescript.ts') + throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); + + const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNodes) as unknown as ts.Statement[]); + + if (!patchSuccess) throw new Error('Failed to patch typescript early return!'); + + return res; + + function visitNodes(node: ts.Statement): ts.VisitResult { + if (isReturnStatement(node)) { + patchSuccess = true; + + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [factory.createVariableDeclaration( + factory.createIdentifier("returnResult"), + undefined, + undefined, + node.expression! + )], + ts.NodeFlags.None + ) + ) + } + + return node; + } + }; +} + +// endregion diff --git a/projects/core/src/patch/transformers/hook-tsc-exec.ts b/projects/core/src/patch/transformers/hook-tsc-exec.ts new file mode 100644 index 0000000..86d8289 --- /dev/null +++ b/projects/core/src/patch/transformers/hook-tsc-exec.ts @@ -0,0 +1,52 @@ +import ts from 'typescript'; +import { execTscCmd } from '../../config'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function hookTscExecTransformer(context: ts.TransformationContext) { + const { factory } = context; + + let patchSuccess = false; + + return (sourceFile: ts.SourceFile) => { + if (sourceFile.fileName !== 'src/tsc/tsc.ts') + throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); + + const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNodes) as unknown as ts.Statement[]); + + if (!patchSuccess) throw new Error('Failed to patch tsc exec statement early return!'); + + return res; + + function visitNodes(node: ts.Statement): ts.VisitResult { + if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'executeCommandLine' + ) { + patchSuccess = true; + + return factory.createExpressionStatement(factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier("tsp"), + factory.createIdentifier("execTsc") + ), + factory.createToken(ts.SyntaxKind.EqualsToken), + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + node.expression + ) + )); + } + + return node; + } + } +} + +// endregion diff --git a/projects/core/src/patch/transformers/index.ts b/projects/core/src/patch/transformers/index.ts new file mode 100644 index 0000000..ded7cff --- /dev/null +++ b/projects/core/src/patch/transformers/index.ts @@ -0,0 +1,6 @@ +export * from './fix-ts-early-return' +export * from './patch-create-program' +export * from './patch-emitter' +export * from './merge-statements' +export * from './add-original-create-program' +export * from './hook-tsc-exec' diff --git a/projects/core/src/patch/transformers/merge-statements.ts b/projects/core/src/patch/transformers/merge-statements.ts new file mode 100644 index 0000000..b1e41a7 --- /dev/null +++ b/projects/core/src/patch/transformers/merge-statements.ts @@ -0,0 +1,61 @@ +import ts from 'typescript'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function createMergeStatementsTransformer( + baseSourceFile: ts.SourceFile, + sourceFile: ts.SourceFile +): ts.TransformerFactory { + const replacements = new Map(); + + for (const node of sourceFile.statements) { + if (ts.isVariableStatement(node)) { + const name = (node.declarationList.declarations[0].name as ts.Identifier).text; + replacements.set(name, node); + } else if (ts.isFunctionDeclaration(node) && node.name) { + const name = node.name.text; + replacements.set(name, node); + } + } + + return (context: ts.TransformationContext) => { + const { factory } = context; + + return (node: ts.SourceFile) => { + if (node.fileName !== baseSourceFile.fileName) return node; + + const transformedStatements: ts.Statement[] = []; + + node.statements.forEach((statement) => { + if (ts.isVariableStatement(statement)) { + const name = (statement.declarationList.declarations[0].name as ts.Identifier).text; + if (replacements.has(name)) { + transformedStatements.push(replacements.get(name)!); + replacements.delete(name); + } else { + transformedStatements.push(statement); + } + } else if (ts.isFunctionDeclaration(statement) && statement.name) { + const name = statement.name.text; + if (replacements.has(name)) { + transformedStatements.push(replacements.get(name)!); + replacements.delete(name); + } else { + transformedStatements.push(statement); + } + } else { + transformedStatements.push(statement); + } + }); + + replacements.forEach((value) => transformedStatements.push(value)); + + return factory.updateSourceFile(node, transformedStatements); + }; + }; +} + +// endregion diff --git a/projects/core/src/patch/transformers/patch-create-program.ts b/projects/core/src/patch/transformers/patch-create-program.ts new file mode 100644 index 0000000..b033d27 --- /dev/null +++ b/projects/core/src/patch/transformers/patch-create-program.ts @@ -0,0 +1,68 @@ +import ts from 'typescript'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function patchCreateProgramTransformer(context: ts.TransformationContext) { + const { factory } = context; + + let patchSuccess = false; + + return (sourceFile: ts.SourceFile) => { + if (sourceFile.fileName !== 'src/compiler/program.ts') + throw new Error('Wrong program file sent to transformer! This should be unreachable.'); + + const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNode) as unknown as ts.Statement[]); + + if (!patchSuccess) throw new Error('Failed to patch createProgram function!'); + + return res; + + function visitNode(node: ts.Statement): ts.VisitResult { + if (ts.isFunctionDeclaration(node) && node.name?.getText() === 'createProgram') { + const originalCreateProgram = factory.updateFunctionDeclaration( + node, + node.modifiers, + node.asteriskToken, + factory.createIdentifier('originalCreateProgram'), + node.typeParameters, + node.parameters, + node.type, + node.body + ); + + // function createProgram() { return tsp.originalCreateProgram(...arguments); } + const newCreateProgram = factory.createFunctionDeclaration( + undefined, + undefined, + 'createProgram', + undefined, + [], + undefined, + factory.createBlock([ + factory.createReturnStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('tsp'), + factory.createIdentifier('createProgram') + ), + undefined, + [ factory.createSpreadElement(factory.createIdentifier('arguments')) ] + ) + ), + ]) + ); + + patchSuccess = true; + + return [ newCreateProgram, originalCreateProgram ]; + } + + return node; + } + } +} + +// endregion diff --git a/projects/core/src/patch/transformers/patch-emitter.ts b/projects/core/src/patch/transformers/patch-emitter.ts new file mode 100644 index 0000000..61ef01f --- /dev/null +++ b/projects/core/src/patch/transformers/patch-emitter.ts @@ -0,0 +1,75 @@ +import ts from 'typescript'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function patchEmitterTransformer(context: ts.TransformationContext) { + const { factory } = context; + + let patchSuccess = false; + + return (sourceFile: ts.SourceFile) => { + if (sourceFile.fileName !== 'src/compiler/watch.ts') + throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); + + const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitRootNodes) as unknown as ts.Statement[]); + + if (!patchSuccess) throw new Error('Failed to patch emitFilesAndReportErrors function!'); + + return res; + + function visitRootNodes(node: ts.Statement): ts.VisitResult { + if (ts.isFunctionDeclaration(node) && node.name && node.name.getText() === 'emitFilesAndReportErrors') { + const newBodyStatements = ts.visitNodes(node.body!.statements, visitEmitterNodes) as unknown as ts.Statement[]; + + return factory.updateFunctionDeclaration( + node, + node.modifiers, + node.asteriskToken, + node.name, + node.typeParameters, + node.parameters, + node.type, + factory.updateBlock(node.body!, newBodyStatements) + ); + } + + return node; + } + + function visitEmitterNodes(node: ts.Statement): ts.VisitResult { + if ( + ts.isVariableStatement(node) && + node.declarationList.declarations.some( + (declaration) => ts.isVariableDeclaration(declaration) && declaration.name.getText() === 'emitResult' + ) + ) { + // tsp.diagnosticMap.set(program, allDiagnostics); + const insertedMapSetterNode = factory.createExpressionStatement(factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('tsp'), + factory.createIdentifier('diagnosticMap') + ), + factory.createIdentifier('set') + ), + undefined, + [ + factory.createIdentifier('program'), + factory.createIdentifier('allDiagnostics') + ] + )); + + patchSuccess = true; + + return [ insertedMapSetterNode, node ]; + } + + return node; + } + }; +} + +// endregion diff --git a/projects/core/src/slice/module-slice.ts b/projects/core/src/slice/module-slice.ts new file mode 100644 index 0000000..826be95 --- /dev/null +++ b/projects/core/src/slice/module-slice.ts @@ -0,0 +1,43 @@ +import { ModuleFile } from '../module'; +import { Position } from '../system'; +import semver from 'semver'; +import { sliceTs5 } from './ts5'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface ModuleSlice { + moduleFile: ModuleFile + firstSourceFileStart: number + wrapperPos?: Position + bodyPos: Position + fileEnd: number + sourceFileStarts: [ name: string, position: number ][] +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function sliceModule(moduleFile: ModuleFile, tsVersion: string) { + if (semver.lte(tsVersion, '5.0.0')) { + throw new Error(`Cannot patch TS version <5`); + } + + /* Handle 5+ */ + return sliceTs5(moduleFile); +} + +/** @internal */ +export namespace ModuleSlice { + export const createError = (msg?: string) => + new Error(`Could not recognize TS format during slice!` + (msg ? ` — ${msg}` : '')); +} + +// endregion + diff --git a/projects/core/src/slice/ts5.ts b/projects/core/src/slice/ts5.ts new file mode 100644 index 0000000..75b1173 --- /dev/null +++ b/projects/core/src/slice/ts5.ts @@ -0,0 +1,72 @@ +import { ModuleFile } from '../module'; +import { ModuleSlice } from './module-slice'; + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function sliceTs5(moduleFile: ModuleFile): ModuleSlice { + let firstSourceFileStart: number; + let wrapperStart: number | undefined; + let wrapperEnd: number | undefined; + let bodyStart: number; + let bodyEnd: number; + let sourceFileStarts: [ name: string, position: number ][] = []; + + const { content } = moduleFile; + + /* Find Wrapper or First File */ + let matcher = /^(?:\s*\/\/\s*src\/)|(?:var\s+ts\s*=.+)/gm; + + const firstMatch = matcher.exec(content); + if (!firstMatch?.[0]) throw ModuleSlice.createError(); + + /* Handle wrapped */ + if (firstMatch[0].startsWith('var')) { + wrapperStart = firstMatch.index; + bodyStart = firstMatch.index + firstMatch[0].length + 1; + + /* Find First File */ + matcher = /^\s*\/\/\s*src\//gm; + matcher.lastIndex = wrapperStart; + + const firstFileMatch = matcher.exec(content); + if (!firstFileMatch?.[0]) throw ModuleSlice.createError(); + + firstSourceFileStart = firstFileMatch.index; + + /* Find Wrapper end */ + matcher = /^}\)\(\)\s*;?/gm; + matcher.lastIndex = firstFileMatch.index; + const wrapperEndMatch = matcher.exec(content); + if (!wrapperEndMatch?.[0]) throw ModuleSlice.createError(); + + bodyEnd = wrapperEndMatch.index - 1; + wrapperEnd = wrapperEndMatch.index + wrapperEndMatch[0].length; + } + /* Handle non-wrapped */ + else { + firstSourceFileStart = firstMatch.index; + bodyStart = firstMatch.index + firstMatch[0].length; + bodyEnd = content.length; + } + + /* Get Source File Positions */ + matcher = /^\s*\/\/\s*(src\/.+)$/gm; + matcher.lastIndex = firstSourceFileStart; + for (let match = matcher.exec(content); match != null; match = matcher.exec(content)) { + sourceFileStarts.push([ match[1], match.index ]); + } + + return { + moduleFile, + firstSourceFileStart, + wrapperPos: wrapperStart != null ? { start: wrapperStart, end: wrapperEnd! } : undefined, + fileEnd: content.length, + bodyPos: { start: bodyStart, end: bodyEnd }, + sourceFileStarts + }; +} + +// endregion diff --git a/projects/core/src/system/cache.ts b/projects/core/src/system/cache.ts new file mode 100644 index 0000000..c03945c --- /dev/null +++ b/projects/core/src/system/cache.ts @@ -0,0 +1,48 @@ +import path from 'path'; +import * as os from 'os'; +import { findCacheDirectory } from '../utils'; +import { appRoot, lockFileDir } from '../config'; +import fs from 'fs'; + + +/* ****************************************************************************************************************** */ +// region: Locals +/* ****************************************************************************************************************** */ + +let cacheRoot: string | undefined; +let lockFileRoot: string | undefined; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getCacheRoot() { + if (!cacheRoot) { + cacheRoot = + process.env.TSP_CACHE_DIR || + findCacheDirectory({ name: 'ts-patch', cwd: path.resolve(appRoot, '..') }) || + path.join(os.tmpdir(), 'ts-patch'); + + if (!fs.existsSync(cacheRoot)) fs.mkdirSync(cacheRoot, { recursive: true }); + } + + return cacheRoot; +} + +export function getCachePath(key: string, ...p: string[]) { + return path.resolve(getCacheRoot(), key, ...p); +} + +export function getLockFilePath(key: string) { + if (!lockFileRoot) { + lockFileRoot = path.join(getCacheRoot(), lockFileDir); + if (!fs.existsSync(lockFileRoot)) fs.mkdirSync(lockFileRoot, { recursive: true }); + } + + return path.join(getCacheRoot(), lockFileDir, key); +} + +// endregion diff --git a/src/installer/lib/system/errors.ts b/projects/core/src/system/errors.ts similarity index 54% rename from src/installer/lib/system/errors.ts rename to projects/core/src/system/errors.ts index 0e97ec6..7f923a8 100644 --- a/src/installer/lib/system/errors.ts +++ b/projects/core/src/system/errors.ts @@ -2,35 +2,37 @@ * Errors Classes * ********************************************************************************************************************/ -export class WrongTSVersion extends Error {name = 'WrongTSVersion'} +export class TspError extends Error { } -export class FileNotFound extends Error {name = 'FileNotFound'} +export class WrongTSVersion extends TspError {name = 'WrongTSVersion'} -export class PackageError extends Error {name = 'PackageError'} +export class FileNotFound extends TspError {name = 'FileNotFound'} -export class PatchError extends Error {name = 'PatchError'} +export class PackageError extends TspError {name = 'PackageError'} -export class PersistenceError extends Error {name = 'PersistenceError'} +export class PatchError extends TspError {name = 'PatchError'} -export class OptionsError extends Error {name = 'OptionsError'} +export class PersistenceError extends TspError {name = 'PersistenceError'} -export class NPMError extends Error {name = 'NPMError'} +export class OptionsError extends TspError {name = 'OptionsError'} -export class RestoreError extends Error { +export class NPMError extends TspError {name = 'NPMError'} + +export class RestoreError extends TspError { constructor(public filename: string, message: string) { super(`Error restoring: ${filename}${message ? ' - ' + message : ''}`); this.name = 'RestoreError'; } } -export class BackupError extends Error { +export class BackupError extends TspError { constructor(public filename: string, message: string) { super(`Error backing up ${filename}${message ? ' - ' + message : ''}`); this.name = 'BackupError'; } } -export class FileWriteError extends Error { +export class FileWriteError extends TspError { constructor(public filename: string, message?: string) { super(`Error while trying to write to ${filename}${message ? `: ${message}` : ''}`); } diff --git a/projects/core/src/system/index.ts b/projects/core/src/system/index.ts new file mode 100644 index 0000000..8e68171 --- /dev/null +++ b/projects/core/src/system/index.ts @@ -0,0 +1,4 @@ +export * from './cache'; +export * from './errors'; +export * from './logger'; +export * from './types'; diff --git a/projects/core/src/system/logger.ts b/projects/core/src/system/logger.ts new file mode 100644 index 0000000..77191df --- /dev/null +++ b/projects/core/src/system/logger.ts @@ -0,0 +1,53 @@ +import chalk from 'chalk'; +import stripAnsi from 'strip-ansi'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export enum LogLevel { + system = 0, + normal = 1, + verbose = 2, +} + +export type Logger = (msg: string | [ string, string ], logLevel?: LogLevel) => void; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function createLogger(logLevel: LogLevel, useColour: boolean = true, isSilent: boolean = false): Logger { + return function log(msg: string | [ string, string ], msgLogLevel: LogLevel = LogLevel.normal) { + if (isSilent || msgLogLevel > logLevel) return; + + /* Handle Icon */ + const printIcon = (icon: string) => chalk.bold.cyanBright(`[${icon}] `); + + let icon: string = ''; + if (Array.isArray(msg)) { + icon = msg[0]; + + // @formatter:off + msg = (icon === '!') ? printIcon(chalk.bold.yellow(icon)) + chalk.yellow(msg[1]) : + (icon === '~') ? printIcon(chalk.bold.cyanBright(icon)) + msg[1] : + (icon === '=') ? printIcon(chalk.bold.greenBright(icon)) + msg[1] : + (icon === '+') ? printIcon(chalk.bold.green(icon)) + msg[1] : + (icon === '-') ? printIcon(chalk.bold.white(icon)) + msg[1] : + msg[1]; + // @formatter:on + } + + /* Print message */ + const isError = (icon === '!'); + + msg = !useColour ? stripAnsi(msg) : msg; + (isError ? console.error : console.log)(msg); + } +} + +// endregion diff --git a/projects/core/src/system/types.ts b/projects/core/src/system/types.ts new file mode 100644 index 0000000..62600a5 --- /dev/null +++ b/projects/core/src/system/types.ts @@ -0,0 +1,11 @@ + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface Position { + start: number + end: number +} + +// endregion diff --git a/projects/core/src/ts-package.ts b/projects/core/src/ts-package.ts new file mode 100644 index 0000000..6747c18 --- /dev/null +++ b/projects/core/src/ts-package.ts @@ -0,0 +1,99 @@ +import fs from 'fs'; +import path from 'path'; +import resolve from 'resolve'; +import { PackageError } from './system'; +import { TsModule } from './module'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface TsPackage { + majorVer: number + minorVer: number + version: string + packageFile: string + packageDir: string + cacheDir: string + libDir: string + + moduleNames: TsModule.Name[] + + /** @internal */ + moduleCache: Map + + getModulePath: (name: TsModule.Name) => string +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +/** + * Get TypeScript package info - Resolve from dir, throws if not cannot find TS package + */ +export function getTsPackage(dir: string = process.cwd()): TsPackage { + if (!fs.existsSync(dir)) throw new PackageError(`${dir} is not a valid directory`); + + const possiblePackageDirs = [ dir, () => path.dirname(resolve.sync(`typescript/package.json`, { basedir: dir })) ]; + + for (const d of possiblePackageDirs) { + let packageDir: string; + try { + packageDir = typeof d === 'function' ? d() : d; + } catch { + break; + } + + /* Parse package.json data */ + const packageFile = path.join(packageDir, 'package.json'); + if (!fs.existsSync(packageFile)) continue; + + const { name, version } = (() => { + try { + return JSON.parse(fs.readFileSync(packageFile, 'utf8')); + } + catch (e) { + throw new PackageError(`Could not parse json data in ${packageFile}`); + } + })(); + + /* Validate */ + if (name === 'typescript') { + const [ sMajor, sMinor ] = version.split('.') + const libDir = path.join(packageDir, 'lib'); + const cacheDir = path.resolve(packageDir, '../.tsp/cache/'); + + /* Get all available module names in libDir */ + const moduleNames: TsModule.Name[] = []; + for (const fileName of fs.readdirSync(libDir)) + if ((TsModule.names).includes(fileName)) moduleNames.push(fileName as TsModule.Name); + + const res: TsPackage = { + version, + majorVer: +sMajor, + minorVer: +sMinor, + packageFile, + packageDir, + moduleNames, + cacheDir, + libDir, + moduleCache: new Map(), + + getModulePath: (moduleName: TsModule.Name) => { + return path.join(libDir, moduleName as string); + } + } + + return res; + } + } + + throw new PackageError(`Could not find typescript package from ${dir}`); +} + +// endregion diff --git a/projects/core/src/utils/file-utils.ts b/projects/core/src/utils/file-utils.ts new file mode 100644 index 0000000..27493c3 --- /dev/null +++ b/projects/core/src/utils/file-utils.ts @@ -0,0 +1,104 @@ +import path from 'path'; +import fs from 'fs'; +import { getTsPackage } from '../ts-package'; +import { getLockFilePath, PackageError } from '../system'; +import { getHash } from './general'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const lockFileWaitMs = 2_000; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +function waitForLockRelease(lockFilePath: string) { + const start = Date.now(); + while (fs.existsSync(lockFilePath)) { + sleep(100); + + if ((Date.now() - start) > lockFileWaitMs) + throw new Error( + `Could not acquire lock to write file. If problem persists, run ts-patch clear-cache and try again. + `); + } + + function sleep(ms: number) { + const wakeUpTime = Date.now() + ms; + while (Date.now() < wakeUpTime) {} + } +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +/** + * Attempts to locate global installation of TypeScript + */ +export function getGlobalTsDir() { + const errors = []; + const dir = require('global-prefix'); + const check = (dir: string) => { + try { return getTsPackage(dir) } + catch (e) { + errors.push(e); + return {}; + } + }; + + const { packageDir } = (check(dir) || check(path.join(dir, 'lib'))); + + if (!packageDir) + throw new PackageError(`Could not find global TypeScript installation! Are you sure it's installed globally?`); + + return packageDir; +} + + +export const mkdirIfNotExist = (dir: string) => !fs.existsSync(dir) && fs.mkdirSync(dir, { recursive: true }); + +export function withFileLock(filePath: string, fn: () => T): T { + const lockFileName = getHash(filePath) + '.lock'; + const lockFilePath = getLockFilePath(lockFileName); + try { + const lockFileDir = path.dirname(lockFilePath); + if (!fs.existsSync(lockFileDir)) fs.mkdirSync(lockFileDir, { recursive: true }); + waitForLockRelease(lockFilePath); + fs.writeFileSync(lockFilePath, ''); + return fn(); + } finally { + fs.unlinkSync(lockFilePath); + } +} + +export function writeFileWithLock(filePath: string, content: string): void { + withFileLock(filePath, () => { + fs.writeFileSync(filePath, content); + }); +} + +export function readFileWithLock(filePath: string): string { + return withFileLock(filePath, () => { + return fs.readFileSync(filePath, 'utf8'); + }); +} + +export function copyFileWithLock(src: string, dest: string): void { + withFileLock(src, () => { + withFileLock(dest, () => { + fs.copyFileSync(src, dest); + }); + }); +} + +// endregion diff --git a/projects/core/src/utils/find-cache-dir.ts b/projects/core/src/utils/find-cache-dir.ts new file mode 100644 index 0000000..2a7d641 --- /dev/null +++ b/projects/core/src/utils/find-cache-dir.ts @@ -0,0 +1,122 @@ +/** + * @credit https://github.com/sindresorhus/find-cache-di + * @license MIT + * @author Sindre Sorhus + * @author James Talmage + * + * MIT License + * + * Copyright (c) Sindre Sorhus (https://sindresorhus.com) + * Copyright (c) James Talmage (https://github.com/jamestalmage) + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * 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. + */ + +import process from 'node:process'; +import path from 'node:path'; +import fs from 'node:fs'; + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface FindCacheDirOptions { + name: string; + cwd?: string; // Default: process.cwd() + create?: boolean; // Default: false +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +const isWritable = (path: string) => { + try { + fs.accessSync(path, fs.constants.W_OK); + return true; + } + catch { + return false; + } +}; + +function useDirectory(directory: string, options: any) { + if (options.create) { + fs.mkdirSync(directory, { recursive: true }); + } + + return directory; +} + +function getNodeModuleDirectory(directory: string) { + const nodeModules = path.join(directory, 'node_modules'); + + if ( + !isWritable(nodeModules) + && (fs.existsSync(nodeModules) || !isWritable(path.join(directory))) + ) { + return; + } + + return nodeModules; +} + +function findNearestPackageDir(startPath: string): string | null { + const visitedDirs = new Set(); + let currentPath = path.resolve(startPath); + + while (true) { + const packageJsonPath = path.join(currentPath, 'package.json'); + + if (fs.existsSync(packageJsonPath)) { + return path.dirname(packageJsonPath); + } + + // Mark the current directory as visited + visitedDirs.add(currentPath); + + // Move to the parent directory + const parentPath = path.dirname(currentPath); + + // Check for a circular loop + if (visitedDirs.has(parentPath) || parentPath === currentPath) { + return null; + } + + currentPath = parentPath; + } +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function findCacheDirectory(options: FindCacheDirOptions) { + /* Use ENV Cache Dir if present */ + if (process.env.CACHE_DIR && ![ 'true', 'false', '1', '0' ].includes(process.env.CACHE_DIR)) + return useDirectory(path.join(process.env.CACHE_DIR, options.name), options); + + /* Find Package Dir */ + const startDir = options.cwd || process.cwd(); + const pkgDir = findNearestPackageDir(startDir); + if (!pkgDir) return undefined; + + /* Find Node Modules Dir */ + const nodeModules = getNodeModuleDirectory(pkgDir); + if (!nodeModules) return undefined; + + return useDirectory(path.join(pkgDir, 'node_modules', '.cache', options.name), options); +} + +// endregion diff --git a/projects/core/src/utils/general.ts b/projects/core/src/utils/general.ts new file mode 100644 index 0000000..d0c5b1f --- /dev/null +++ b/projects/core/src/utils/general.ts @@ -0,0 +1,24 @@ +import crypto from "crypto"; + + +/* ****************************************************************************************************************** */ +// region: Type Utils +/* ****************************************************************************************************************** */ + +/** + * Make certain properties partial + */ +export type PartialSome = Omit & Pick, K> + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Crypto Utils +/* ****************************************************************************************************************** */ + +export function getHash(fileContent: string) { + return crypto.createHash('md5').update(fileContent).digest('hex'); +} + +// endregion diff --git a/projects/core/src/utils/index.ts b/projects/core/src/utils/index.ts new file mode 100644 index 0000000..60aefb7 --- /dev/null +++ b/projects/core/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './general'; +export * from './file-utils'; +export * from './find-cache-dir'; diff --git a/projects/core/tsconfig.json b/projects/core/tsconfig.json new file mode 100644 index 0000000..fba1f7f --- /dev/null +++ b/projects/core/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends" : "../../tsconfig.base.json", + "include" : [ "shared", "src" ], + + "compilerOptions" : { + "rootDirs" : [ "src", "shared" ], + "outDir" : "../../dist", + "sourceMap" : true, + "composite" : true, + "declaration" : true, + + "plugins" : [ + { + "transform" : "./plugin.ts", + "transformProgram" : true + } + ] + } +} diff --git a/src/patch/package.json b/projects/patch/package.json similarity index 100% rename from src/patch/package.json rename to projects/patch/package.json diff --git a/src/patch/plugin.ts b/projects/patch/plugin.ts similarity index 74% rename from src/patch/plugin.ts rename to projects/patch/plugin.ts index 30aeef1..29e633a 100644 --- a/src/patch/plugin.ts +++ b/projects/patch/plugin.ts @@ -1,5 +1,5 @@ import type * as ts from 'typescript' -import type { ProgramTransformerExtras } from '../shared/plugin-types'; +import type { ProgramTransformerExtras } from 'ts-patch'; import * as fs from 'fs'; import * as path from 'path'; @@ -8,8 +8,8 @@ import * as path from 'path'; // region: Config /* ****************************************************************************************************************** */ -const srcTypesFileName = path.resolve(__dirname, '../shared/plugin-types.ts'); -const destTypesFileName = path.resolve(__dirname, 'lib/types/plugin-types.ts'); +const srcTypesFileName = path.resolve(__dirname, '../core/shared/plugin-types.ts'); +const destTypesFileName = path.resolve(__dirname, 'src/types/plugin-types.ts'); // endregion @@ -29,7 +29,7 @@ function transformPatchDeclarationsFile(this: typeof ts, ctx: ts.TransformationC this.isModuleDeclaration(node) && this.getJSDocTags(node).some(t => t.tagName.text === 'build-types') ) .map((node: ts.ModuleDeclaration) => - factory.updateModuleDeclaration(node, node.decorators, node.modifiers, moduleName, node.body) + factory.updateModuleDeclaration(node, node.modifiers, moduleName, node.body) ); return factory.updateSourceFile( @@ -45,15 +45,16 @@ function transformPluginTypes(this: typeof ts, ctx: ts.TransformationContext) { return (sourceFile: ts.SourceFile) => { const moduleDeclaration = factory.createModuleDeclaration( - undefined, [ factory.createModifier(this.SyntaxKind.DeclareKeyword) ], factory.createIdentifier('tsp'), factory.createModuleBlock( sourceFile .statements - .filter(node => this.isDeclaration(node) && this.getCombinedModifierFlags(node) & this.ModifierFlags.Export) + // TODO - remove the casting once we have tsei again + .filter(node => (this).isDeclaration(node) && this.getCombinedModifierFlags(node as unknown as ts.Declaration)) ), - this.NodeFlags.Namespace | this.NodeFlags.ExportContext | this.NodeFlags.Ambient | this.NodeFlags.ContextFlags + // TODO - remove the casting once we have tsei again + this.NodeFlags.Namespace | this.NodeFlags.ExportContext | (this.NodeFlags).Ambient | this.NodeFlags.ContextFlags ); return factory.updateSourceFile(sourceFile, [ moduleDeclaration ]); @@ -75,9 +76,13 @@ export function transformProgram( ) { host ??= ts.createCompilerHost(program.getCompilerOptions(), true); - const printer = ts.createPrinter({ removeComments: true }); - const srcFileName = ts.normalizePath(srcTypesFileName); - const destFileName = ts.normalizePath(destTypesFileName); + const printer = ts.createPrinter({ + removeComments: true, + newLine: ts.NewLineKind.LineFeed + }); + // TODO - remove the casting once we have tsei again + const srcFileName = (ts).normalizePath(srcTypesFileName); + const destFileName = (ts).normalizePath(destTypesFileName); hookWriteFile(); generatePluginTypesAndInjectToProgram(); @@ -94,14 +99,17 @@ export function transformProgram( host.writeFile = (fileName: string, data: string, ...args: any[]) => { /* Transform declarations */ if (/module-patch.d.ts$/.test(fileName)) { - let sourceFile = ts.createSourceFile(fileName, data, ts.ScriptTarget.ES2015, true); + let sourceFile = ts.createSourceFile(fileName, data, ts.ScriptTarget.ES2016, true, ts.ScriptKind.TS); sourceFile = ts.transform(sourceFile, [ transformPatchDeclarationsFile.bind(ts) ]).transformed[0]; return (originalWriteFile)(fileName, printer.printFile(sourceFile), ...args); } /* Strip comments from js */ if (/module-patch.js$/.test(fileName)) { - const sourceFile = ts.createSourceFile(fileName, data, ts.ScriptTarget.ES2015, false); + /* Wrap file in closure */ + data = `var tsp = (function() {\n${data}\nreturn tsp;})();`; + + const sourceFile = ts.createSourceFile(fileName, data, ts.ScriptTarget.ES2016, false, ts.ScriptKind.JS); return (originalWriteFile)(fileName, printer.printFile(sourceFile), ...args); } @@ -113,8 +121,8 @@ export function transformProgram( let sourceFile = ts.createSourceFile(srcFileName, fs.readFileSync(srcFileName, 'utf8'), ts.ScriptTarget.ES2015, true); sourceFile = ts.transform(sourceFile, [ transformPluginTypes.bind(ts) ]).transformed[0]; - const moduleBody = `/** AUTO-GENERATED - DO NOT EDIT */\n\n/** @build-types */\n` + printer.printFile(sourceFile); - sourceFile = ts.createSourceFile(destFileName, moduleBody, ts.ScriptTarget.ES2015, true); + const moduleBody = `// @ts-nocheck\n/** AUTO-GENERATED - DO NOT EDIT */\n\n/** @build-types */\n` + printer.printFile(sourceFile); + fs.writeFileSync(destFileName, moduleBody); } } diff --git a/projects/patch/src/plugin/esm-intercept.ts b/projects/patch/src/plugin/esm-intercept.ts new file mode 100644 index 0000000..fb4e8b5 --- /dev/null +++ b/projects/patch/src/plugin/esm-intercept.ts @@ -0,0 +1,124 @@ +namespace tsp { + const Module = require('module'); + const path = require('path'); + const fs = require('fs'); + const crypto = require('crypto'); + + /* ********************************************************* */ + // region: Helpers + /* ********************************************************* */ + + function getEsmLibrary() { + try { + return require('esm') as typeof import('esm'); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') + throw new TsPatchError( + `Plugin is an ESM module. To enable experimental ESM support, ` + + `install the 'esm' package as a (dev)-dependency or global.` + ); + else throw e; + } + } + + // endregion + + /* ********************************************************* */ + // region: Utils + /* ********************************************************* */ + + export function registerEsmIntercept(registerConfig: RegisterConfig): () => void { + const originalRequire = Module.prototype.require; + const originalReadFileSync = fs.readFileSync; + const builtFiles = new Map(); + + const getHash = () => { + let hash: string; + do { + hash = crypto.randomBytes(16).toString('hex'); + } while (builtFiles.has(hash)); + + return hash; + } + + /* Create cleanup function */ + const cleanup = () => { + /* Cleanup temp ESM files */ + for (const { 1: filePath } of builtFiles) { + delete require.cache[filePath]; + try { + fs.readFileSync(filePath, 'utf8'); + } catch (e) { + if (process.env.NODE_ENV !== 'production') + console.warn(`[ts-patch] Warning: Failed to delete temporary esm cache file: ${filePath}.`); + } + } + + builtFiles.clear(); + Module.prototype.require = originalRequire; + } + + /* Set Hooks */ + try { + Module.prototype.require = wrappedRequire; + } catch (e) { + cleanup(); + } + + /* ********************************************************* * + * Helpers + * ********************************************************* */ + + function wrappedRequire(this: unknown, request: string) { + try { + return originalRequire.apply(this, arguments); + } catch (e) { + if (e.code === 'ERR_REQUIRE_ESM') { + const resolvedPath = Module._resolveFilename(request, this, false); + const resolvedPathExt = path.extname(resolvedPath); + + if (Module._cache[resolvedPath]) return Module._cache[resolvedPath].exports; + + /* Compile TS */ + let targetFilePath: string; + if (tsExtensions.includes(resolvedPathExt)) { + if (!builtFiles.has(resolvedPath)) { + const tsCode = originalReadFileSync(resolvedPath, 'utf8'); + const jsCode = registerConfig.tsNodeInstance!.compile(tsCode, resolvedPath); + const outputFileName = getHash() + '.mjs'; + const outputFilePath = path.join(getTmpDir('esm'), outputFileName); + fs.writeFileSync(outputFilePath, jsCode, 'utf8'); + + builtFiles.set(resolvedPath, outputFilePath); + targetFilePath = outputFilePath; + } else { + targetFilePath = builtFiles.get(resolvedPath)!; + } + } else { + targetFilePath = resolvedPath; + } + + /* Setup new module */ + const newModule = new Module(request, this); + newModule.filename = resolvedPath; + newModule.paths = Module._nodeModulePaths(resolvedPath); + + /* Add to cache */ + Module._cache[resolvedPath] = newModule; + + /* Load with ESM library */ + const res = getEsmLibrary()(newModule)(targetFilePath); + newModule.filename = resolvedPath; + + return res; + } + + throw e; + } + } + + return cleanup; + } + + // endregion +} diff --git a/projects/patch/src/plugin/plugin-creator.ts b/projects/patch/src/plugin/plugin-creator.ts new file mode 100644 index 0000000..2935873 --- /dev/null +++ b/projects/patch/src/plugin/plugin-creator.ts @@ -0,0 +1,201 @@ +namespace tsp { + const crypto = require('crypto'); + + /* ********************************************************* */ + // region: Types + /* ********************************************************* */ + + /** @internal */ + interface CreateTransformerFromPatternOptions { + factory: PluginFactory; + config: PluginConfig; + registerConfig: RegisterConfig; + program: tsShim.Program; + ls?: tsShim.LanguageService; + } + + // endregion + + /* ********************************************************* */ + // region: Helpers + /* ********************************************************* */ + + function validateConfigs(configs: PluginConfig[]) { + for (const config of configs) + if (!config.name && !config.transform) throw new TsPatchError('tsconfig.json plugins error: transform must be present'); + } + + function createTransformerFromPattern(opt: CreateTransformerFromPatternOptions): TransformerBasePlugin { + const { factory, config, program, ls, registerConfig } = opt; + const { transform, after, afterDeclarations, name, type, transformProgram, ...cleanConfig } = config; + + if (!transform) throw new TsPatchError('Not a valid config entry: "transform" key not found'); + + let transformerFn: PluginFactory; + switch (config.type) { + case 'ls': + if (!ls) throw new TsPatchError(`Plugin ${transform} needs a LanguageService`); + transformerFn = (factory as LSPattern)(ls, cleanConfig); + break; + + case 'config': + transformerFn = (factory as ConfigPattern)(cleanConfig); + break; + + case 'compilerOptions': + transformerFn = (factory as CompilerOptionsPattern)(program.getCompilerOptions(), cleanConfig); + break; + + case 'checker': + transformerFn = (factory as TypeCheckerPattern)(program.getTypeChecker(), cleanConfig); + break; + + case undefined: + case 'program': + const { addDiagnostic, removeDiagnostic, diagnostics } = diagnosticExtrasFactory(program); + + transformerFn = (factory as ProgramPattern)(program, cleanConfig, { + ts: ts, + addDiagnostic, + removeDiagnostic, + diagnostics, + library: tsp.currentLibrary + }); + break; + + case 'raw': + transformerFn = (ctx: tsShim.TransformationContext) => (factory as RawPattern)(ctx, program, cleanConfig); + break; + + default: + throw new TsPatchError(`Invalid plugin type found in tsconfig.json: '${config.type}'`); + } + + /* Wrap w/ register */ + const wrapper = wrapTransformer(transformerFn, registerConfig, true); + + const res: TransformerBasePlugin = + after ? ({ after: wrapper }) : + afterDeclarations ? ({ afterDeclarations: wrapper as tsShim.TransformerFactory }) : + { before: wrapper }; + + return res; + } + + function wrapTransformer( + transformerFn: T, + requireConfig: RegisterConfig, + wrapInnerFunction: boolean + ): T { + const wrapper = function tspWrappedFactory(...args: any[]) { + let res: any; + try { + registerPlugin(requireConfig); + if (!wrapInnerFunction) { + res = (transformerFn as Function)(...args); + } else { + const resFn = (transformerFn as Function)(...args); + if (typeof resFn !== 'function') throw new TsPatchError('Invalid plugin: expected a function'); + res = wrapTransformer(resFn, requireConfig, false); + } + } finally { + unregisterPlugin(); + } + + return res; + } as T; + + return wrapper; + } + + // endregion + + /* ********************************************************* * + * PluginCreator (Class) + * ********************************************************* */ + + /** + * @example + * + * new PluginCreator([ + * {transform: '@zerollup/ts-transform-paths', someOption: '123'}, + * {transform: '@zerollup/ts-transform-paths', type: 'ls', someOption: '123'}, + * {transform: '@zerollup/ts-transform-paths', type: 'ls', after: true, someOption: '123'} + * ]).createTransformers({ program }) + */ + export class PluginCreator { + constructor( + private configs: PluginConfig[], + public resolveBaseDir: string = process.cwd() + ) + { + validateConfigs(configs); + } + + public mergeTransformers(into: TransformerList, source: tsShim.CustomTransformers | TransformerBasePlugin) { + const slice = (input: T | T[]) => (Array.isArray(input) ? input.slice() : [ input ]); + + if (source.before) into.before.push(...slice(source.before)); + if (source.after) into.after.push(...slice(source.after)); + if (source.afterDeclarations) into.afterDeclarations.push(...slice(source.afterDeclarations)); + + return this; + } + + public createTransformers( + params: { program: tsShim.Program } | { ls: tsShim.LanguageService }, + customTransformers?: tsShim.CustomTransformers + ): TransformerList { + const transformers: TransformerList = { before: [], after: [], afterDeclarations: [] }; + + const [ ls, program ] = ('ls' in params) ? [ params.ls, params.ls.getProgram()! ] : [ void 0, params.program ]; + + for (const config of this.configs) { + if (!config.transform || config.transformProgram) continue; + + const resolvedFactory = tsp.resolveFactory(this, config); + if (!resolvedFactory) continue; + + const { factory, registerConfig } = resolvedFactory; + + this.mergeTransformers( + transformers, + createTransformerFromPattern({ + factory: factory as PluginFactory, + registerConfig, + config, + program, + ls + }) + ); + } + + // Chain custom transformers at the end + if (customTransformers) this.mergeTransformers(transformers, customTransformers); + + return transformers; + } + + public getProgramTransformers(): Map { + const res = new Map(); + for (const config of this.configs) { + if (!config.transform || !config.transformProgram) continue; + + const resolvedFactory = resolveFactory(this, config); + if (resolvedFactory === undefined) continue; + + const { registerConfig } = resolvedFactory; + const factory = wrapTransformer(resolvedFactory.factory as ProgramTransformer, registerConfig, false); + + const transformerKey = crypto + .createHash('md5') + .update(JSON.stringify({ factory, config })) + .digest('hex'); + + res.set(transformerKey, [ factory, config ]); + } + + return res; + } + } +} diff --git a/projects/patch/src/plugin/register-plugin.ts b/projects/patch/src/plugin/register-plugin.ts new file mode 100644 index 0000000..5135ed3 --- /dev/null +++ b/projects/patch/src/plugin/register-plugin.ts @@ -0,0 +1,139 @@ +/// + +namespace tsp { + const path = require('path'); + + let configStack: RegisterConfig[] = []; + + /* ********************************************************* */ + // region: Types + /* ********************************************************* */ + + /** @internal */ + export interface RegisterConfig { + tsNodeInstance?: import('ts-node').Service + tsConfigPathsCleanup?: () => void + esmInterceptCleanup?: () => void + isTs: boolean + pluginConfig: PluginConfig + isEsm: boolean + tsConfig: string | undefined + compilerOptions?: tsShim.CompilerOptions + } + + // endregion + + /* ********************************************************* */ + // region: Helpers + /* ********************************************************* */ + + function getTsNode() { + try { + return require('ts-node') as typeof import('ts-node'); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') + throw new TsPatchError( + `Cannot use a typescript-based transformer without ts-node installed. `+ + `Add ts-node as a (dev)-dependency or install globally.` + ); + else throw e; + } + } + + function getTsConfigPaths() { + try { + return require('tsconfig-paths') as typeof import('tsconfig-paths'); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') + throw new TsPatchError( + `resolvePathAliases requires the library: tsconfig-paths. `+ + `Add tsconfig-paths as a (dev)-dependency or install globally.` + ); + else throw e; + } + } + + function getCompilerOptions(tsConfig: string) { + const configFile = tsShim.readConfigFile(tsConfig, tsShim.sys.readFile); + const parsedConfig = configFile && tsShim.parseJsonConfigFileContent( + configFile.config, + tsShim.sys, + path.dirname(tsConfig) + ); + + return parsedConfig.options; + } + + // endregion + + /* ********************************************************* */ + // region: Utils + /* ********************************************************* */ + + export function unregisterPlugin() { + const activeRegisterConfig = configStack.pop()!; + + if (activeRegisterConfig.tsConfigPathsCleanup) { + activeRegisterConfig.tsConfigPathsCleanup(); + delete activeRegisterConfig.tsConfigPathsCleanup; + } + + if (activeRegisterConfig.tsNodeInstance) { + activeRegisterConfig.tsNodeInstance.enabled(false); + } + + if (activeRegisterConfig.esmInterceptCleanup) { + activeRegisterConfig.esmInterceptCleanup(); + delete activeRegisterConfig.esmInterceptCleanup; + } + } + + export function registerPlugin(registerConfig: RegisterConfig) { + if (!registerConfig) throw new TsPatchError('requireConfig is required'); + configStack.push(registerConfig); + + const { isTs, isEsm, tsConfig, pluginConfig } = registerConfig; + + /* Register ESM */ + if (isEsm) { + registerConfig.esmInterceptCleanup = registerEsmIntercept(registerConfig); + } + + /* Register tsNode */ + if (isTs) { + const tsNode = getTsNode(); + + let tsNodeInstance: import('ts-node').Service; + if (registerConfig.tsNodeInstance) { + tsNodeInstance = registerConfig.tsNodeInstance; + tsNode.register(tsNodeInstance); + } else { + tsNodeInstance = tsNode.register({ + transpileOnly: true, + ...(tsConfig ? { project: tsConfig } : { skipProject: true }), + compilerOptions: { + target: isEsm ? 'ESNext' : 'ES2018', + jsx: 'react', + esModuleInterop: true, + module: isEsm ? 'ESNext' : 'commonjs', + } + }); + } + + tsNodeInstance.enabled(true); + registerConfig.tsNodeInstance = tsNodeInstance; + } + + /* Register tsconfig-paths */ + if (tsConfig && pluginConfig.resolvePathAliases) { + registerConfig.compilerOptions ??= getCompilerOptions(tsConfig); + + const { paths, baseUrl } = registerConfig.compilerOptions; + if (paths && baseUrl) { + registerConfig.tsConfigPathsCleanup = getTsConfigPaths().register({ baseUrl, paths }); + } + } + } + + // endregion +} diff --git a/projects/patch/src/plugin/resolve-factory.ts b/projects/patch/src/plugin/resolve-factory.ts new file mode 100644 index 0000000..e120292 --- /dev/null +++ b/projects/patch/src/plugin/resolve-factory.ts @@ -0,0 +1,117 @@ +namespace tsp { + const path = require('path'); + + const requireStack: string[] = []; + + /* ********************************************************* */ + // region: Types + /* ********************************************************* */ + + /** @internal */ + export interface ResolveFactoryResult { + factory: PluginFactory | ProgramTransformer + registerConfig: RegisterConfig + } + + // endregion + + /* ********************************************************* */ + // region: Utils + /* ********************************************************* */ + + export function resolveFactory(pluginCreator: PluginCreator, pluginConfig: PluginConfig): ResolveFactoryResult | undefined { + const tsConfig = pluginConfig.tsConfig && path.resolve(pluginCreator.resolveBaseDir, pluginConfig.tsConfig); + const transform = pluginConfig.transform!; + const importKey = pluginConfig.import || 'default'; + const transformerPath = require.resolve(transform, { paths: [ pluginCreator.resolveBaseDir ] }); + + if (pluginConfig.resolvePathAliases && !tsConfig) { + console.warn(`[ts-patch] Warning: resolvePathAliases needs a tsConfig value pointing to a tsconfig.json for transformer" ${transform}.`); + } + + /* Prevent circular require */ + if (requireStack.includes(transformerPath)) return; + requireStack.push(transformerPath); + + /* Check if ESM */ + let isEsm: boolean | undefined = pluginConfig.isEsm; + if (isEsm == null) { + const impliedModuleFormat = tsShim.getImpliedNodeFormatForFile( + transformerPath as tsShim.Path, + undefined, + tsShim.sys, + { moduleResolution: tsShim.ModuleResolutionKind.Node16 } + ); + + isEsm = impliedModuleFormat === tsShim.ModuleKind.ESNext; + } + + const isTs = transform!.match(/\.[mc]?ts$/) != null; + + const registerConfig: RegisterConfig = { + isTs, + isEsm, + tsConfig: tsConfig, + pluginConfig + }; + + registerPlugin(registerConfig); + + try { + /* Load plugin */ + const commonjsModule = loadPlugin(); + + const factoryModule = (typeof commonjsModule === 'function') ? { default: commonjsModule } : commonjsModule; + const factory = factoryModule[importKey]; + + if (!factory) + throw new TsPatchError( + `tsconfig.json > plugins: "${transform}" does not have an export "${importKey}": ` + + require('util').inspect(factoryModule) + ); + + if (typeof factory !== 'function') { + throw new TsPatchError( + `tsconfig.json > plugins: "${transform}" export "${importKey}" is not a plugin: ` + + require('util').inspect(factory) + ); + } + + return { + factory, + registerConfig: registerConfig, + }; + } + finally { + requireStack.pop(); + unregisterPlugin(); + } + + function loadPlugin(): PluginFactory | { [key: string]: PluginFactory } { + /* Load plugin */ + let res: PluginFactory | { [key: string]: PluginFactory } + try { + res = require(transformerPath); + } catch (e) { + if (e.code === 'ERR_REQUIRE_ESM') { + if (!registerConfig.isEsm) { + unregisterPlugin(); + registerConfig.isEsm = true; + registerPlugin(registerConfig); + return loadPlugin(); + } else { + throw new TsPatchError( + `Cannot load ESM transformer "${transform}" from "${transformerPath}". Please file a bug report` + ); + } + } + else throw e; + } + return res; + } + } + + // endregion +} + +// endregion diff --git a/projects/patch/src/shared.ts b/projects/patch/src/shared.ts new file mode 100644 index 0000000..3c3e3aa --- /dev/null +++ b/projects/patch/src/shared.ts @@ -0,0 +1,54 @@ +namespace tsp { + const os = require('os'); + const path = require('path'); + const fs = require('fs'); + + /* ********************************************************* */ + // region: Vars + /* ********************************************************* */ + + export const diagnosticMap: tsp.DiagnosticMap = new WeakMap(); + + /** Injected during patch — library name minus extension */ + export declare const currentLibrary: string; + + export const supportedExtensions = [ '.ts', '.mts', '.cts', '.js', '.mjs', '.cjs' ]; + export const tsExtensions = [ '.ts', '.mts', '.cts' ]; + + // endregion + + /* ********************************************************* */ + // region: Utils + /* ********************************************************* */ + + /** @internal */ + export function diagnosticExtrasFactory(program: tsShim.Program) { + const diagnostics = diagnosticMap.get(program) || diagnosticMap.set(program, []).get(program)!; + + const addDiagnostic = (diag: tsShim.Diagnostic): number => diagnostics.push(diag); + const removeDiagnostic = (index: number) => { diagnostics.splice(index, 1) }; + + return { addDiagnostic, removeDiagnostic, diagnostics }; + } + + /** @internal */ + export function getTmpDir(subPath?: string) { + const tmpDir = path.resolve(os.tmpdir(), 'tsp', subPath); + if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true }); + return tmpDir; + } + + // endregion + + /* ********************************************************* */ + // region: Other + /* ********************************************************* */ + + export class TsPatchError extends Error { + constructor(message: string, public diagnostic?: tsShim.Diagnostic) { + super(message); + } + } + + // endregion +} diff --git a/src/patch/lib/createProgram.ts b/projects/patch/src/ts/create-program.ts similarity index 59% rename from src/patch/lib/createProgram.ts rename to projects/patch/src/ts/create-program.ts index 3b0847a..f889e9f 100644 --- a/src/patch/lib/createProgram.ts +++ b/projects/patch/src/ts/create-program.ts @@ -1,27 +1,22 @@ -/* - * The logic in this file is based on TTypescript (https://github.com/cevek/ttypescript) - * Credit & thanks go to cevek (https://github.com/cevek) for the incredible work! - */ - namespace tsp { - const activeProgramTransformers = new Set(); + const activeProgramTransformers = new Set(); const { dirname } = require('path'); - /* ********************************************************* * - * Helpers - * ********************************************************* */ + /* ********************************************************* */ + // region: Helpers + /* ********************************************************* */ - function getProjectDir(compilerOptions: ts.CompilerOptions) { + function getProjectDir(compilerOptions: tsShim.CompilerOptions) { return compilerOptions.configFilePath && dirname(compilerOptions.configFilePath); } - function getProjectConfig(compilerOptions: ts.CompilerOptions, rootFileNames: ReadonlyArray) { + function getProjectConfig(compilerOptions: tsShim.CompilerOptions, rootFileNames: ReadonlyArray) { let configFilePath = compilerOptions.configFilePath; let projectDir = getProjectDir(compilerOptions); if (configFilePath === undefined) { const baseDir = (rootFileNames.length > 0) ? dirname(rootFileNames[0]) : projectDir ?? process.cwd(); - configFilePath = ts.findConfigFile(baseDir, ts.sys.fileExists); + configFilePath = tsShim.findConfigFile(baseDir, tsShim.sys.fileExists); if (configFilePath) { const config = readConfig(configFilePath); @@ -35,11 +30,11 @@ namespace tsp { function readConfig(configFileNamePath: string) { const projectDir = dirname(configFileNamePath); - const result = ts.readConfigFile(configFileNamePath, ts.sys.readFile); + const result = tsShim.readConfigFile(configFileNamePath, tsShim.sys.readFile); - if (result.error) throw new Error('Error in tsconfig.json: ' + result.error.messageText); + if (result.error) throw new TsPatchError('Error in tsconfig.json: ' + result.error.messageText); - return ts.parseJsonConfigFileContent(result.config, ts.sys, projectDir, undefined, configFileNamePath); + return tsShim.parseJsonConfigFileContent(result.config, tsShim.sys, projectDir, undefined, configFileNamePath); } function preparePluginsFromCompilerOptions(plugins: any): PluginConfig[] { @@ -58,21 +53,23 @@ namespace tsp { return plugins; } + // endregion + /* ********************************************************* * * Patched createProgram() * ********************************************************* */ export function createProgram( - rootNamesOrOptions: ReadonlyArray | ts.CreateProgramOptions, - options?: ts.CompilerOptions, - host?: ts.CompilerHost, - oldProgram?: ts.Program, - configFileParsingDiagnostics?: ReadonlyArray - ): ts.Program { + rootNamesOrOptions: ReadonlyArray | tsShim.CreateProgramOptions, + options?: tsShim.CompilerOptions, + host?: tsShim.CompilerHost, + oldProgram?: tsShim.Program, + configFileParsingDiagnostics?: ReadonlyArray + ): tsShim.Program { let rootNames; /* Determine options */ - const createOpts = !Array.isArray(rootNamesOrOptions) ? rootNamesOrOptions : void 0; + const createOpts = !Array.isArray(rootNamesOrOptions) ? rootNamesOrOptions : void 0; if (createOpts) { rootNames = createOpts.rootNames; options = createOpts.options; @@ -86,35 +83,34 @@ namespace tsp { /* Get Config */ const projectConfig = getProjectConfig(options, rootNames); - if (tsp.isTSC) { + if ([ 'tsc', 'tsserver', 'tsserverlibrary' ].includes(tsp.currentLibrary)) { options = projectConfig.compilerOptions; if (createOpts) createOpts.options = options; } /* Invoke TS createProgram */ - let program: ts.Program & { originalEmit?: ts.Program['emit'] } = + let program: tsShim.Program & { originalEmit?: tsShim.Program['emit'] } = createOpts ? - ts.originalCreateProgram(createOpts) : - ts.originalCreateProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics); + tsShim.originalCreateProgram(createOpts) : + tsShim.originalCreateProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics); /* Prepare Plugins */ const plugins = preparePluginsFromCompilerOptions(options.plugins); const pluginCreator = new PluginCreator(plugins, projectConfig.projectDir ?? process.cwd()); /* Prevent recursion in Program transformers */ - const programTransformers = new Map(pluginCreator.getProgramTransformers()); - for (const [ transformer ] of pluginCreator.getProgramTransformers()) { - if (activeProgramTransformers.has(transformer)) programTransformers.delete(transformer); - else activeProgramTransformers.add(transformer); - } + const programTransformers = pluginCreator.getProgramTransformers(); /* Transform Program */ - for (const [ programTransformer, config ] of programTransformers) { + for (const [ transformerKey, [ programTransformer, config ] ] of programTransformers) { + if (activeProgramTransformers.has(transformerKey)) continue; + activeProgramTransformers.add(transformerKey); + const newProgram: any = programTransformer(program, host, config, { ts: ts }); if (typeof newProgram?.['emit'] === 'function') program = newProgram; - } - programTransformers.forEach((c, transformer) => activeProgramTransformers.delete(transformer)); + activeProgramTransformers.delete(transformerKey); + } /* Hook emit method */ if (!program.originalEmit) { @@ -123,18 +119,18 @@ namespace tsp { } function newEmit( - targetSourceFile?: ts.SourceFile, - writeFile?: ts.WriteFileCallback, - cancellationToken?: ts.CancellationToken, + targetSourceFile?: tsShim.SourceFile, + writeFile?: tsShim.WriteFileCallback, + cancellationToken?: tsShim.CancellationToken, emitOnlyDtsFiles?: boolean, - customTransformers?: ts.CustomTransformers, + customTransformers?: tsShim.CustomTransformers, ...additionalArgs: any - ): ts.EmitResult { + ): tsShim.EmitResult { /* Merge in our transformers */ const transformers = pluginCreator.createTransformers({ program }, customTransformers); /* Invoke TS emit */ - const result: ts.EmitResult = program.originalEmit!( + const result: tsShim.EmitResult = program.originalEmit!( targetSourceFile, writeFile, cancellationToken, @@ -146,7 +142,7 @@ namespace tsp { /* Merge in transformer diagnostics */ for (const diagnostic of tsp.diagnosticMap.get(program) || []) - if (!result.diagnostics.includes(diagnostic)) (result.diagnostics).push(diagnostic) + if (!result.diagnostics.includes(diagnostic)) (result.diagnostics).push(diagnostic) return result; } diff --git a/projects/patch/src/ts/shim.ts b/projects/patch/src/ts/shim.ts new file mode 100644 index 0000000..9325565 --- /dev/null +++ b/projects/patch/src/ts/shim.ts @@ -0,0 +1,48 @@ +/** @internal */ +namespace tsp { + declare const ts: typeof import('typescript') | undefined; + + /** + * Compensate for modules which do not wrap functions in a `ts` namespace. + */ + export const tsShim = new Proxy( + {}, + { + get(_, key: string) { + if (ts) { + return (ts)[key]; + } else { + try { + return eval(key); + } catch (e) { + return undefined; + } + } + }, + } + ) as typeof import('typescript') + /* Temporary until tsei works again */ + & { + normalizePath: any; + fixupCompilerOptions: any; + } + + export namespace tsShim { + export type CompilerOptions = import('typescript').CompilerOptions; + export type CreateProgramOptions = import('typescript').CreateProgramOptions; + export type Program = import('typescript').Program; + export type CompilerHost = import('typescript').CompilerHost; + export type Diagnostic = import('typescript').Diagnostic; + export type SourceFile = import('typescript').SourceFile; + export type WriteFileCallback = import('typescript').WriteFileCallback; + export type CancellationToken = import('typescript').CancellationToken; + export type CustomTransformers = import('typescript').CustomTransformers; + export type EmitResult = import('typescript').EmitResult; + export type LanguageService = import('typescript').LanguageService; + export type TransformationContext = import('typescript').TransformationContext; + export type Node = import('typescript').Node; + export type TransformerFactory = import('typescript').TransformerFactory; + export type Bundle = import('typescript').Bundle; + export type Path = import('typescript').Path; + } +} diff --git a/src/patch/lib/types/plugin-types.ts b/projects/patch/src/types/plugin-types.ts similarity index 96% rename from src/patch/lib/types/plugin-types.ts rename to projects/patch/src/types/plugin-types.ts index a3ffb44..2c8e526 100644 --- a/src/patch/lib/types/plugin-types.ts +++ b/projects/patch/src/types/plugin-types.ts @@ -1,3 +1,4 @@ +// @ts-nocheck /** AUTO-GENERATED - DO NOT EDIT */ /** @build-types */ @@ -6,13 +7,14 @@ declare namespace tsp { [x: string]: any; name?: string; transform?: string; + resolvePathAliases?: boolean; tsConfig?: string; import?: string; + isEsm?: boolean; type?: 'ls' | 'program' | 'config' | 'checker' | 'raw' | 'compilerOptions'; after?: boolean; afterDeclarations?: boolean; transformProgram?: boolean; - beforeEmit?: boolean; } export type TransformerList = Required; export type TransformerPlugin = TransformerBasePlugin | ts.TransformerFactory; diff --git a/projects/patch/src/types/typescript.ts b/projects/patch/src/types/typescript.ts new file mode 100644 index 0000000..1b7c6b1 --- /dev/null +++ b/projects/patch/src/types/typescript.ts @@ -0,0 +1,11 @@ +/* ****************************************************************************************************************** * + * Added Properties + * ****************************************************************************************************************** */ + +/** @build-types */ +declare namespace ts { + /** @internal */ + const createProgram: typeof import('typescript').createProgram; + + export const originalCreateProgram: typeof ts.createProgram; +} diff --git a/src/patch/tsconfig.json b/projects/patch/tsconfig.json similarity index 83% rename from src/patch/tsconfig.json rename to projects/patch/tsconfig.json index 3aaddb8..e6db858 100644 --- a/src/patch/tsconfig.json +++ b/projects/patch/tsconfig.json @@ -1,5 +1,7 @@ { - "include": [ "lib" ], + "include": [ + "src" + ], "compilerOptions": { "outFile": "../../dist/resources/module-patch.js", @@ -10,11 +12,9 @@ "noUnusedLocals": false, "noImplicitReturns": true, "allowSyntheticDefaultImports": true, - "allowSyntheticDefaultImports": true, "stripInternal": true, - "target": "ES5", - "lib": [ "es2015", "es5" ], + "target": "ES2020", "downlevelIteration": true, "useUnknownInCatchVariables": false, "newLine": "LF", diff --git a/scripts/postbuild.js b/scripts/postbuild.js index eb0719f..355e4cb 100644 --- a/scripts/postbuild.js +++ b/scripts/postbuild.js @@ -13,12 +13,19 @@ const BASE_DIR = path.resolve('.'); const SRC_DIR = path.resolve('./src'); const DIST_DIR = path.resolve('./dist'); const DIST_RESOURCE_DIR = path.join(DIST_DIR, 'resources'); +const COMPILER_DIR = path.resolve('./projects/core/src/compiler'); /* ******************************************************************************************************************** * Post-Build Steps * ********************************************************************************************************************/ +/* Uncomment this if you need to temporarily build without patched ts */ +// shell.mv(path.join(DIST_DIR, 'src', '*'), DIST_DIR); +// shell.mv(path.join(DIST_DIR, 'shared', '*'), DIST_DIR); +// shell.rm('-rf', path.join(DIST_DIR, 'src')); +// shell.rm('-rf', path.join(DIST_DIR, 'shared')); + /* Build package.json */ const pkgJSON = JSON.parse(fs.readFileSync(path.join(BASE_DIR, 'package.json'), 'utf8')); @@ -33,15 +40,26 @@ fs.writeFileSync( JSON.stringify(pkgJSON, null, 2).replace(/(?<=['"].*?)dist\//g, '') ); +/* Copy Live files */ +shell.cp('-r', COMPILER_DIR, DIST_DIR); + /* Copy Readme & Changelog */ shell.cp(path.resolve('./README.md'), DIST_DIR); shell.cp(path.resolve('./CHANGELOG.md'), DIST_DIR); shell.cp(path.resolve('./LICENSE.md'), DIST_DIR); -/* Add shebang line to cli */ -const cliPath = path.join(DIST_DIR, '/bin/cli.js'); -const cliSrc = fs.readFileSync(cliPath, 'utf8'); -if (!/^#!\/usr\/bin\/env/.test(cliSrc)) fs.writeFileSync(cliPath, `#!/usr/bin/env node\n\n${cliSrc}`, 'utf8'); +/* Add shebang line to bin files */ +const binFiles = glob + .sync(path.join(DIST_DIR, 'bin', '*.js')) + .map((filePath) => [ + filePath, + `#!/usr/bin/env node\n\n` + fs.readFileSync(filePath, 'utf8') + ]); + +for (const [ filePath, fileContent] of binFiles) { + const fileName = path.basename(filePath); + fs.writeFileSync(path.join(DIST_DIR, 'bin', fileName), fileContent, 'utf8'); +} /* Ensure EOL = LF in resources */ const resFiles = glob.sync(path.join(DIST_RESOURCE_DIR, '*').replace(/\\/g, '/')); diff --git a/src/installer/bin/cli.ts b/src/installer/bin/cli.ts deleted file mode 100644 index cdfbafb..0000000 --- a/src/installer/bin/cli.ts +++ /dev/null @@ -1,165 +0,0 @@ -import minimist from 'minimist'; -import chalk from 'chalk'; -import stripAnsi from 'strip-ansi'; -import * as actions from '../lib/actions' -import { appOptions, Log, parseOptions, TSPOptions, tspPackageJSON } from '../lib/system'; -import { getTSPackage } from '../lib/file-utils'; - - -/* ******************************************************************************************************************** - * Commands & Options - * ********************************************************************************************************************/ -// region Commands & Options - -type MenuData = Record; - -export const cliOptions: MenuData = { - silent: { short: 's', caption: 'Run silently' }, - global: { short: 'g', caption: 'Target global TypeScript installation' }, - verbose: { short: 'v', caption: 'Chat it up' }, - dir: { short: 'd', paramCaption: '', caption: 'TypeScript directory or directory to resolve typescript package from' }, - color: { inverse: true, caption: 'Strip ansi colours from output' } -}; - -export const cliCommands: MenuData = { - install: { short: 'i', caption: `Installs ts-patch (to main libraries)` }, - uninstall: { short: 'u', caption: 'Restores original typescript files' }, - check: { - short: 'c', caption: - `Check patch status (use with ${chalk.cyanBright('--dir')} to specify TS package location)` - }, - patch: { - short: void 0, paramCaption: ' | ', caption: - 'Patch specific module(s) ' + chalk.yellow('(advanced)') - }, - unpatch: { - short: void 0, paramCaption: ' | ', caption: - 'Un-patch specific module(s) ' + chalk.yellow('(advanced)') - }, - version: { short: 'v', caption: 'Show version' }, - help: { short: '/?', caption: 'Show help menu' }, -}; - -// endregion - - -/* ******************************************************************************************************************** - * Menu - * ********************************************************************************************************************/ -// region Menu - -const LINE_INDENT = '\r\n\t'; -const COL_WIDTH = 45; - -const formatLine = (left: (string | undefined)[], caption: string, paramCaption: string = '') => { - const leftCol = left.filter(Boolean).join(chalk.blue(', ')) + ' ' + chalk.yellow(paramCaption); - const dots = chalk.grey('.'.repeat(COL_WIDTH - stripAnsi(leftCol).length)); - return `${leftCol} ${dots} ${caption}`; -}; - -const menuText = - LINE_INDENT + chalk.bold.blue('ts-patch [command] ') + chalk.blue('') + '\r\n' + LINE_INDENT + - - // Commands - Object - .entries(cliCommands) - .map(([ cmd, { short, caption, paramCaption } ]) => formatLine([ cmd, short ], caption, paramCaption)) - .join(LINE_INDENT) + - - // Options - '\r\n' + LINE_INDENT + chalk.bold('Options') + LINE_INDENT + - Object - .entries(cliOptions) - .map(([ long, { short, inverse, caption, paramCaption } ]) => formatLine([ - short && `${chalk.cyanBright('-' + short)}`, - long && `${chalk.cyanBright(`${inverse ? '--no-' : '--'}${long}`)}` - ], caption, paramCaption)) - .join(LINE_INDENT); - - -// endregion - - -/* ******************************************************************************************************************** - * App - * ********************************************************************************************************************/ -// region App - -const instanceIsCLI = (require.main === module); - -if (instanceIsCLI) run(); - -export function run(argStr?: string) { - let args = minimist(instanceIsCLI ? process.argv.slice(2) : argStr!.split(' ')); - let cmd: string | undefined = args._[0] ? args._[0].toLowerCase() : void 0; - - try { - /* Select command by short or full code */ - if (cmd) cmd = (Object.keys(cliCommands).includes(cmd)) ? cmd : - (Object.entries(cliCommands).find(([ n, { short } ]) => n && (short === cmd)) || [])[0]; - - /* Parse options (convert short-code to long) */ - const opts = Object - .entries(cliOptions) - .reduce((p, [ key, { short } ]) => ({ - ...p, - ...(args.hasOwnProperty(key) && { [key]: args[key] }), - ...(short && args.hasOwnProperty(short) && { [key]: args[short] }) - }), >{}); - - if (args.basedir) opts.basedir = args.basedir; - - /* Handle special cases */ - if ((args.v) && (!cmd)) cmd = 'version'; - if (args.h) cmd = 'help'; - if (args.colour !== undefined) opts.color = args.colour; - - /* Build & Handle options */ - parseOptions({ instanceIsCLI, ...opts }); - - /* Handle commands */ - (() => { - switch (cmd) { - case 'help': - return Log(menuText, Log.system); - - case 'version': - const { version: tsVersion, packageDir } = getTSPackage(appOptions.dir); - return Log('\r\n' + - chalk.bold.blue('ts-patch: ') + tspPackageJSON.version + '\r\n' + - chalk.bold.blue('typescript: ') + tsVersion + chalk.gray(` [${packageDir}]`), - Log.system - ); - - case 'install': - return actions.install(); - - case 'uninstall': - return actions.uninstall(); - - case 'patch': - return actions.patch(args._.slice(1).join(' '), { verbose: true }); - - case 'unpatch': - return actions.unpatch(args._.slice(1).join(' '), { verbose: true }); - - case 'check': - return actions.check(); - - default: - Log('Invalid command. Try ts-patch /? for more info', Log.system) - } - })(); - } - catch (e) { - Log([ - '!', - chalk.bold.yellow(e.name && (e.name !== 'Error') ? `[${e.name}]: ` : 'Error: ') + chalk.red(e.message) - ], Log.system); - } - - // Output for analysis by tests - return (!instanceIsCLI) ? ({ cmd, args, options: appOptions }) : void 0; -} - -// endregion diff --git a/src/installer/index.ts b/src/installer/index.ts deleted file mode 100644 index aa8acc4..0000000 --- a/src/installer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { getTSPackage, getTSModule, TSPackage, TSModule } from './lib/file-utils'; -export { TSPOptions } from './lib/system/options'; -export { install, uninstall, patch, setOptions } from './lib/actions' -export * from './plugin-types' diff --git a/src/installer/lib/actions.ts b/src/installer/lib/actions.ts deleted file mode 100644 index 5cca7df..0000000 --- a/src/installer/lib/actions.ts +++ /dev/null @@ -1,267 +0,0 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import glob from 'glob'; -import chalk from 'chalk'; -import * as shell from 'shelljs'; -import { patchTSModule } from './patcher'; -import { getModuleAbsolutePath, getTSModule, getTSPackage, mkdirIfNotExist, TSModule, TSPackage } from './file-utils'; -import { - appRoot, BackupError, defineProperties, Log, parseOptions, PatchError, resetOptions, RestoreError, TSPOptions, - tspPackageJSON -} from './system'; - - -/* ******************************************************************************************************************** - * Config - * ********************************************************************************************************************/ -// region Config - -shell.config.silent = true; - -export const SRC_FILES = [ 'tsc.js', 'tsserverlibrary.js', 'typescript.js', 'typescriptServices.js', 'tsserver.js' ]; -export const BACKUP_DIRNAME = 'lib-backup'; -export const RESOURCES_PATH = path.join(appRoot, tspPackageJSON.directories.resources); - -export const defaultInstallLibraries = [ 'tsc.js', 'typescript.js' ]; - -// endregion - - -/* ******************************************************************************************************************** - * Helpers - * ********************************************************************************************************************/ -// region Helpers - -/** - * Parse file, array of files, or glob of files and get TSModule info for each - */ -export function parseFiles(fileOrFilesOrGlob: string | string[], libDir: string, includeSrc: boolean = false) { - const files = - Array.isArray(fileOrFilesOrGlob) ? fileOrFilesOrGlob : - fs.existsSync(getModuleAbsolutePath(fileOrFilesOrGlob, libDir)) ? [ fileOrFilesOrGlob ] : - glob.sync(fileOrFilesOrGlob); - - const ret = files.map(f => getTSModule(getModuleAbsolutePath(f, libDir), includeSrc)); - - return defineProperties(ret, { - patched: { get: () => ret.filter(f => f.patchVersion) }, - unPatchable: { get: () => ret.filter(f => !f.canPatch) }, - canUpdateOrPatch: { get: () => ret.filter(f => f.canPatch && f.outOfDate) }, - patchable: { get: () => ret.filter(f => f.canPatch) }, - }); -} - -/** - * Create backup of TS Module file - */ -function backup(tsModule: TSModule, tsPackage: TSPackage) { - const backupDir = path.join(tsPackage.packageDir, BACKUP_DIRNAME); - - if (tsModule.patchVersion) - throw new Error(`Cannot backup an already patched module. You may need to reinstall typescript.`) - - try { mkdirIfNotExist(backupDir) } - catch (e) { throw new BackupError(tsModule.filename, `Couldn't create backup directory. ${e.message}`); } - - if (shell.cp(tsModule.file, backupDir) && shell.error()) - throw new BackupError(tsModule.filename, shell.error()); - - if (tsModule.filename === 'typescript.js') - if (shell.cp(path.join(tsModule.dir, 'typescript.d.ts'), backupDir) && shell.error()) - throw new BackupError('typescript.d.ts', shell.error()); -} - -/** - * Restore module from backup - */ -function restore(currentModule: TSModule, tsPackage: TSPackage, noDelete?: boolean) { - const copyOrMove = (fileName: string, dest: string) => - shell[noDelete ? 'cp' : 'mv'](fileName, dest); - - const backupDir = path.join(tsPackage.packageDir, BACKUP_DIRNAME); - const { file, filename, canPatch, patchVersion, dir } = getTSModule(path.join(backupDir, currentModule.filename)); - - /* Verify backup file */ - if (!canPatch) throw new RestoreError(filename, `Backup file is not a valid typescript module!`); - if (patchVersion) throw new RestoreError(filename, `Backup file is not an un-patched ts module`); - - /* Restore files */ - if (copyOrMove(file, tsPackage.libDir) && shell.error()) - throw new RestoreError(filename, `Couldn't restore file - ${shell.error()}`); - - if (filename === 'typescript.js') - if (copyOrMove(path.join(dir, 'typescript.d.ts'), tsPackage.libDir) && shell.error()) - throw new RestoreError(filename, `Couldn't restore file - ${shell.error()}`); - - /* Verify restored file */ - const restoredModule = getTSModule(currentModule.file); - if (!restoredModule.canPatch) throw new RestoreError(filename, - `Restored file is not a valid typescript module! You will need to reinstall typescript.` - ); - if (restoredModule.patchVersion) throw new RestoreError(filename, - `Restored file still has patch! You will need to reinstall typescript.` - ); - - // Remove backup dir if empty - if ((fs.readdirSync(backupDir).length < 1) && shell.rm('-rf', backupDir) && shell.error()) - Log([ '!', `Error deleting backup directory` + chalk.grey(`[${backupDir}]`) ], Log.verbose); -} - -// endregion - - -/* ******************************************************************************************************************** - * Actions - * ********************************************************************************************************************/ -// region Actions - -/** - * Set app options (superimposes opts onto defaultOptions) - */ -export const setOptions = (opts?: Partial) => resetOptions(opts); - -/** - * Patch TypeScript modules - */ -export function install(opts?: Partial) { - const ret = patch(defaultInstallLibraries, opts); - if (ret) Log([ '+', chalk.green(`ts-patch installed!`) ]); - return ret; -} - -/** - * Remove patches from TypeScript modules - */ -export function uninstall(opts?: Partial) { - const ret = unpatch(defaultInstallLibraries, opts); - if (ret) Log([ '-', chalk.green(`ts-patch removed!`) ]); - return ret; -} - -/** - * Check if files can be patched - */ -export function check(fileOrFilesOrGlob: string | string[] = SRC_FILES, opts?: Partial) { - const { dir } = parseOptions(opts); - const { libDir, packageDir, version } = getTSPackage(dir); - - Log(`Checking TypeScript ${chalk.blueBright(`v${version}`)} installation in ${chalk.blueBright(packageDir)}\r\n`); - - const modules = parseFiles(fileOrFilesOrGlob, libDir); - for (const module of modules) { - const { filename, patchVersion, canPatch, outOfDate } = module; - if (patchVersion) Log([ '+', - `${chalk.blueBright(filename)} is patched with ts-patch version ` + - `${chalk[outOfDate ? 'redBright' : 'blueBright'](patchVersion)} ${outOfDate ? '(out of date)' : '' }` - ]); - else if (canPatch) Log([ '-', `${chalk.blueBright(filename)} is not patched.` ]); - else Log([ '-', chalk.red(`${chalk.redBright(filename)} is not patched and cannot be patched!`) ]); - - Log('', Log.verbose); - } - - return modules; -} - -/** - * Patch a TypeScript module - */ -export function patch(fileOrFilesOrGlob: string | string[], opts?: Partial) { - if (!fileOrFilesOrGlob) throw new PatchError(`Must provide a file path, array of files, or glob.`); - const { dir } = parseOptions(opts); - - const tsPackage = getTSPackage(dir); - const modules = parseFiles(fileOrFilesOrGlob, tsPackage.libDir, true); - - if (!modules.canUpdateOrPatch.length) { - Log([ '!', - `File${modules.length-1 ? 's' : ''} already patched with the latest version. For details, run: ` + - chalk.bgBlackBright('ts-patch check') - ]); - return false; - } - - /* Patch files */ - for (let m of modules.canUpdateOrPatch) { - const { file, filename } = m; - Log([ '~', `Patching ${chalk.blueBright(filename)} in ${chalk.blueBright(path.dirname(file))}` ], Log.verbose); - - // If already patched, load backup module source. Otherwise, backup un-patched - if (m.patchVersion) m.moduleSrc = - getTSModule(path.join(tsPackage.packageDir, BACKUP_DIRNAME, m.filename), /* includeSrc */ true).moduleSrc; - else backup(m, tsPackage); - - patchTSModule(m, tsPackage); - tsPackage.config.modules[filename] = fs.statSync(file).mtimeMs; - - Log([ '+', chalk.green(`Successfully patched ${chalk.bold.yellow(filename)}.\r\n`) ], Log.verbose); - } - - tsPackage.config.save(); - - if (modules.unPatchable.length > 1) { - Log([ '!', - `Some files can't be patched! Try updating to a newer version of ts-patch. The following files are unable to be ` + - `patched. [${modules.unPatchable.map(f => f.filename).join(', ')}]` - ]); - return false; - } - - return true; -} - -export function unpatch(fileOrFilesOrGlob: string | string[], opts?: Partial) { - if (!fileOrFilesOrGlob) throw new PatchError(`Must provide a file path, array of files, or glob.`); - const { dir, verbose, instanceIsCLI } = parseOptions(opts); - - const tsPackage = getTSPackage(dir); - const modules = parseFiles(fileOrFilesOrGlob, tsPackage.libDir, true); - - if (modules.patched.length < 1) { - Log([ '!', `File${modules.length-1 ? 's' : ''} not patched. For details, run: ` + chalk.bgBlackBright('ts-patch check') ]); - return false; - } - - /* Restore files */ - const errors: Record = {}; - for (let tsModule of modules.patched) { - const { file, filename } = tsModule; - Log([ '~', `Restoring ${chalk.blueBright(filename)} in ${chalk.blueBright(path.dirname(file))}` ], Log.verbose); - - try { - restore(tsModule, tsPackage); - delete tsPackage.config.modules[filename]; - - Log([ '+', chalk.green(`Successfully restored ${chalk.bold.yellow(filename)}.\r\n`) ], Log.verbose); - } - catch (e) { - errors[filename] = e; - } - } - - /* Save config, or handle if no patched files left */ - if (Object.keys(tsPackage.config.modules).length > 0) tsPackage.config.save(); - else { - // Remove ts-patch.json file - shell.rm('-rf', tsPackage.config.file); - } - - /* Handle errors */ - if (Object.keys(errors).length > 0) { - Object.values(errors).forEach(e => { - if (!instanceIsCLI) console.warn(e); - else Log([ '!', e.message ], Log.verbose) - }); - - Log(''); - throw new RestoreError( - `[${Object.keys(errors).join(', ')}]`, - 'Try reinstalling typescript.' + - (!verbose ? ' (Or, run uninstall again with --verbose for specific error detail)' : '') - ); - } - - return true; -} - -// endregion diff --git a/src/installer/lib/file-utils.ts b/src/installer/lib/file-utils.ts deleted file mode 100644 index f35da31..0000000 --- a/src/installer/lib/file-utils.ts +++ /dev/null @@ -1,210 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import { - appOptions, defineProperties, FileNotFound, FileWriteError, isAbsolute, Log, PackageError, tspPackageJSON -} from './system'; -import resolve from 'resolve'; - - -/* ******************************************************************************************************************** - * Helpers - * ********************************************************************************************************************/ -// region Helpers - -/** - * Attempts to locate global installation of TypeScript - */ -export const getGlobalTSDir = () => { - const errors = []; - const dir = require('global-prefix'); - const check = (dir: string) => { - try { return getTSPackage(dir) } - catch (e) { - errors.push(e); - return {}; - } - }; - - const { packageDir } = (check(dir) || check(path.join(dir, 'lib'))); - - if (!packageDir) - throw new PackageError(`Could not find global TypeScript installation! Are you sure it's installed globally?`); - - return packageDir; -}; - -/** - * Get absolute path for module file - */ -export const getModuleAbsolutePath = (filename: string, libDir: string) => { - let file = isAbsolute(filename) ? filename : path.join(libDir, filename); - if (path.extname(file) !== '.js') file = path.join(path.dirname(file), `${path.basename(file, path.extname(file))}.js`); - - return file; -}; - -export const mkdirIfNotExist = (dir: string) => !fs.existsSync(dir) && fs.mkdirSync(dir, { recursive: true }); - -const isOutOfDate = (version: any) => { - const currentVer = tspPackageJSON.version.match(/(\d+)\.(\d+)\.(\d+)/); - // noinspection JSUnusedLocalSymbols - const [ f, major, minor, patch ] = String(version).match(/(\d+)\.(\d+)\.(\d+)/) || [] as string[]; - - return (isNaN(+major) || isNaN(+minor) || isNaN(+patch)) || - (currentVer?.[1] > major) || (currentVer?.[2] > minor) || (currentVer?.[3] > patch); -} - -// endregion - - -/* ******************************************************************************************************************** - * TS Package - * ********************************************************************************************************************/ - -// region TS Package - -export interface TSPackage { - version: string, - packageFile: string, - packageDir: string, - config: TSPConfig, - libDir: string -} - -/** - * Get TypeScript package info - Resolve from dir, throws if not cannot find TS package - */ -export function getTSPackage(dir: string = process.cwd()): TSPackage { - if (!fs.existsSync(dir)) throw new PackageError(`${dir} is not a valid directory`); - - const possiblePackageDirs = [ dir, () => path.dirname(resolve.sync(`typescript/package.json`, { basedir: dir })) ]; - - for (const d of possiblePackageDirs) { - let packageDir: string; - try { - packageDir = typeof d === 'function' ? d() : d; - } catch { - break; - } - - /* Parse package.json data */ - const packageFile = path.join(packageDir, 'package.json'); - if (!fs.existsSync(packageFile)) continue; - - const { name, version } = (() => { - try { - return JSON.parse(fs.readFileSync(packageFile, 'utf8')); - } - catch (e) { - throw new PackageError(`Could not parse json data in ${packageFile}`); - } - })(); - - /* Validate */ - if (name === 'typescript') - return { version, packageFile, packageDir, config: getConfig(packageDir), libDir: path.join(packageDir, 'lib') }; - } - - throw new PackageError(`Could not find typescript package from ${dir}`); -} - -// endregion - - -/* ******************************************************************************************************************** - * TS Module - * ********************************************************************************************************************/ - -// region TS Module - -export interface TSModule { - filename: string, - file: string, - dir: string, - canPatch: boolean, - patchVersion: string | false | null, - moduleSrc?: string - outOfDate: boolean -} - -/** - * Get TypeScript module info - */ -export function getTSModule(file: string, includeSrc: boolean = false): TSModule { - if (!fs.existsSync(file)) throw new FileNotFound(`Could not find file ${file}.`); - - const filename = path.basename(file); - const dir = path.dirname(file); - const fileData = fs.readFileSync(file, 'utf8'); - const canPatch = Boolean(fileData.match(/^\(function\s\(ts\)\s?{[\s\S]+?\(ts\s?\|\|\s?\(ts\s?=\s?{}\)\);?$/m)); - const patchVersion = - canPatch && ( - fileData.match(/^\/\/\/\s*tsp:\s*(\S+)/s) || - fileData.match(/(?<=^\s*?var\stspVersion\s?=\s?['"`])(\S+?)(?=['"`])/m) || - [] - )[1]; - - const outOfDate = isOutOfDate(patchVersion); - - return { file, filename, canPatch, dir, patchVersion, outOfDate, ...(includeSrc && canPatch && { moduleSrc: fileData }) }; -} - -// endregion - - -/* ******************************************************************************************************************** - * TSP Config - * ********************************************************************************************************************/ - -// region TSP Config - -export interface TSPConfig { - readonly file: string, - readonly version: string, - modules: { [x: string]: number } - - save: Function; -} - -/** - * Load tsp config file data from TS package directory - */ -function getConfig(packageDir: string) { - const configFile = path.join(packageDir, 'ts-patch.json'); - - /* Load config file */ - let fileData = >{}; - if (fs.existsSync(configFile)) { - try { - fileData = JSON.parse(fs.readFileSync(configFile, 'utf8')); - } - catch (e) { - if (appOptions.instanceIsCLI) console.warn(e); - else Log([ '!', e.message ], Log.verbose) - } - } - - const config: TSPConfig = { - modules: {}, - ...fileData, - version: fileData.version || tspPackageJSON.version, - file: configFile, - save() { saveConfig(this) } - }; - - return defineProperties(config, { - version: { writable: false }, - file: { enumerable: false, writable: false } - }); -} - -function saveConfig(config: TSPConfig) { - try { - fs.writeFileSync(config.file, JSON.stringify(config, null, 2)); - } - catch (e) { - throw new FileWriteError(config.file, e.message); - } -} - -// endregion diff --git a/src/installer/lib/patcher.ts b/src/installer/lib/patcher.ts deleted file mode 100644 index 962e7a0..0000000 --- a/src/installer/lib/patcher.ts +++ /dev/null @@ -1,157 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { appRoot, FileNotFound, FileWriteError, PatchError, tspPackageJSON, WrongTSVersion } from './system'; -import { getTSModule, TSModule, TSPackage } from './file-utils'; -import { BACKUP_DIRNAME } from './actions'; - - -/* ****************************************************************************************************************** */ -// region: Constants -/* ****************************************************************************************************************** */ - -const dtsPatchSrc = '\n' + - fs.readFileSync(path.resolve(appRoot, tspPackageJSON.directories.resources, 'module-patch.d.ts'), 'utf-8'); - -const jsPatchSrc = - fs.readFileSync(path.resolve(appRoot, tspPackageJSON.directories.resources, 'module-patch.js'), 'utf-8') - -// endregion - - -/* ******************************************************************************************************************** - * Helpers - * ********************************************************************************************************************/ - -const getHeader = () => `/// tsp: ${tspPackageJSON.version}\n\n`; - -/** - * Generate insertion code for module-patch - */ -const generatePatch = (isTSC: boolean) => - jsPatchSrc + - `\nObject.assign(tsp, { isTSC: ${isTSC}, tspVersion: '${tspPackageJSON.version}' });\n\n`; - -/** - * Validate TSModule and TSPackage before patching - */ -function validate(tsModule?: TSModule, tsPackage?: TSPackage) { - if (tsModule) { - const { file, filename, dir, patchVersion, canPatch, outOfDate } = tsModule; - - if (!fs.existsSync(file)) throw new FileNotFound(`Could not find module ${filename} in ${dir + path.sep}`); - - if (patchVersion && !outOfDate) - throw new PatchError(`Module ${filename} is already up-to-date with local version - v${patchVersion}`); - if (!canPatch) throw new PatchError(`Module ${filename} cannot be patched! No instance of TypeScript found.`); - } - - if (tsPackage) { - const [ major ] = tsPackage.version.split('.'); - if (+major < 4) throw new WrongTSVersion(`ts-patch v2 requires TypeScript v4.0 or higher.`); - } - - return true; -} - -const patchModuleDiagnostics = (tsModule: TSModule, tsPackage: TSPackage, source?: string) => { - const src = source || tsModule.moduleSrc!; - - const funcPos = src.search(/function emitFilesAndReportErrors\(/); - if (funcPos < 0) throw new Error(`Bad TS Code. Could not find function emitFilesAndReportErrors in ${tsModule.filename}`); - - const startCode = src.substr(0, funcPos); - const restCode = src.substr(funcPos); - - /* Find emit call position */ - const emitPos = restCode.search(/^\s*?var emitResult =/m); - if (emitPos < 0) throw new Error(`Could not determine emit call. Please file an issue with your TS version!`); - - const ver = tsPackage.version.split('.') - const majorVer = +ver[0]; - const minorVer = +ver[1]; - - const diagnosticsArrName = 'allDiagnostics'; - - return startCode + - restCode.substr(0, emitPos) + - `\nts.diagnosticMap.set(program, ${diagnosticsArrName});\n` + - restCode.substr(emitPos); -} - - -/* ******************************************************************************************************************** - * Patch - * ********************************************************************************************************************/ - -/** - * Patch TypeScript Module - */ -export function patchTSModule(tsModule: TSModule, tsPackage: TSPackage) { - validate(tsModule, tsPackage); - - const { filename, file, dir } = tsModule; - - /* Install patch */ - const isTSC = (filename === 'tsc.js'); - const patchSrc = generatePatch(isTSC); - - /* Add diagnostic modification support */ - const moduleSrc = patchModuleDiagnostics(tsModule, tsPackage); - - try { - if (isTSC) { - /* Select non-patched typescript.js */ - const tsFile = - [ - path.join(tsPackage.packageDir, BACKUP_DIRNAME, 'typescript.js'), - path.join(tsPackage.libDir, 'typescript.js') - ] - .filter(f => fs.existsSync(f))[0]; - - /* Get TSC-specific module slice */ - const ver = tsPackage.version.split('.') - const majorVer = +ver[0]; - const minorVer = +ver[1]; - - let tscSlice = majorVer >= 4 && minorVer >= 9 - ? moduleSrc.replace(/^[\s\S]+(\(function \(ts\) {\s+var StatisticType;[\s\S]+)$/, '$1') - : moduleSrc.replace(/^[\s\S]+(\(function \(ts\) {\s+function countLines[\s\S]+)$/, '$1'); - const execCmdPost = tscSlice.lastIndexOf('\nts.executeCommandLine\('); - if (execCmdPost < 0) throw new Error(`Could not find tsc executeCommandLine`); - const execCmd = tscSlice.slice(execCmdPost); - tscSlice = tscSlice.slice(0, execCmdPost); - - /* Expand TSC with full typescript library (splice tsc part on top of typescript.ts code) */ - const content = Buffer.concat([ - Buffer.from(getHeader()), - Buffer.from(patchModuleDiagnostics(tsModule, tsPackage, fs.readFileSync(tsFile, 'utf-8'))), - Buffer.from(!getTSModule(tsFile).patchVersion ? patchSrc : ''), - Buffer.from(tscSlice), - Buffer.from(execCmd) - ]); - - fs.writeFileSync(file, content); - } else fs.writeFileSync(file, Buffer.concat([ - Buffer.from(getHeader()), - Buffer.from(moduleSrc), - Buffer.from(patchSrc) - ])); - } - catch (e) { - throw new FileWriteError(filename, e.message); - } - - /* Patch d.ts with types (if module is typescript.ts) */ - if (filename === 'typescript.js') { - const targetFile = path.join(dir, 'typescript.d.ts'); - const backupFile = path.join(tsPackage.packageDir, BACKUP_DIRNAME, 'typescript.d.ts'); - - try { - if (fs.existsSync(backupFile)) fs.writeFileSync(targetFile, fs.readFileSync(backupFile, 'utf-8') + dtsPatchSrc) - else fs.appendFileSync(targetFile, dtsPatchSrc) - } - catch (e) { - throw new FileWriteError(filename, (e as Error).stack); - } - } -} diff --git a/src/installer/lib/system/helpers.ts b/src/installer/lib/system/helpers.ts deleted file mode 100644 index b04e8c8..0000000 --- a/src/installer/lib/system/helpers.ts +++ /dev/null @@ -1,76 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - - -/* ******************************************************************************************************************** - * General Helpers - * ********************************************************************************************************************/ - -/** - * Determine if path is absolute (works on windows and *nix) - */ -export const isAbsolute = (sPath: string) => - path.resolve(sPath) === path.normalize(sPath).replace(RegExp(path.sep + '$'), ''); - -/** - * Filter object to only include entries by keys (Based on TypeScript Pick) - * @param obj - Object to filter - * @param keys - Keys to extract - * @example - * let obj = { a: 1, b: 2, c: '3' } // Type is { a: number, b: number, c: string } - * obj = pick(obj, 'a', 'b') // Type is { a: number, c: string } - */ -export function pick(obj: T, ...keys: K[]): Pick { - return { - ...keys.reduce((p, key) => ({ - ...p, - ...((obj as any).hasOwnProperty(key) && { [key]: obj[key] }) - }), {}) - } as Pick; -} - -/** - * Fully typed Object.keys - */ -export const getKeys = (obj: T): Array => Object.keys(obj) as Array; - -type GetDescriptorType any }> = - 'value' extends keyof T ? T['value'] : - T['get'] extends Function ? ReturnType : - T['set'] extends Function ? Parameters[0] : - T['initializer'] extends Function ? ReturnType : - never; - -/** - * Fully typed Object.defineProperties - */ -export function defineProperties>(obj: TObj, properties: TProps): - TObj & { [K in keyof TProps]: GetDescriptorType } { - return Object.defineProperties(obj, properties) as any; -} - - -/* ******************************************************************************************************************** - * TS Patch - * ********************************************************************************************************************/ - -/** - * Root directory for ts-patch - */ -export const appRoot = (() => { - const moduleDir = path.join(__dirname, '../../'); - - const chkFile = (pkgFile: string) => - (fs.existsSync(pkgFile) && (require(pkgFile).name === 'ts-patch')) ? path.dirname(pkgFile) : void 0; - - const res = chkFile(path.join(moduleDir, 'package.json')) || chkFile(path.join(moduleDir, '../../package.json')); - - if (!res) throw new Error(`Error getting app root. No valid ts-patch package file found in ` + moduleDir); - - return res; -})(); - -/** - * Package json for ts-patch - */ -export const tspPackageJSON = require(path.resolve(appRoot, 'package.json')); diff --git a/src/installer/lib/system/index.ts b/src/installer/lib/system/index.ts deleted file mode 100644 index 3f8f89a..0000000 --- a/src/installer/lib/system/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './errors'; -export * from './helpers'; -export * from './options'; -export * from './logger'; \ No newline at end of file diff --git a/src/installer/lib/system/logger.ts b/src/installer/lib/system/logger.ts deleted file mode 100644 index 38598d6..0000000 --- a/src/installer/lib/system/logger.ts +++ /dev/null @@ -1,45 +0,0 @@ -import chalk from 'chalk'; -import { appOptions } from './options'; -import stripAnsi from 'strip-ansi'; - - -/* ******************************************************************************************************************** - * Logger - * ********************************************************************************************************************/ - -/** - * Output log message - */ -export function Log(msg: string | [ string, string ], logLevel: typeof Log[keyof typeof Log] = Log.normal) { - if (logLevel > appOptions.logLevel) return; - const { color, instanceIsCLI } = appOptions; - - /* Handle Icon */ - const printIcon = (icon: string) => chalk.bold.cyanBright(`[${icon}] `); - - let icon: string = ''; - if (Array.isArray(msg)) { - icon = msg[0]; - msg = (icon === '!') ? printIcon(chalk.bold.yellow(icon)) + chalk.yellow(msg[1]) : - (icon === '~') ? printIcon(chalk.bold.cyanBright(icon)) + msg[1] : - (icon === '=') ? printIcon(chalk.bold.greenBright(icon)) + msg[1] : - (icon === '+') ? printIcon(chalk.bold.green(icon)) + msg[1] : - (icon === '-') ? printIcon(chalk.bold.white(icon)) + msg[1] : - msg[1]; - } - const isError = (icon === '!'); - - /* Print message */ - msg = !color ? stripAnsi(msg) : msg; - - if (!instanceIsCLI) console.log(msg); - else if (isError) process.stderr.write(msg + '\r\n'); - else process.stdout.write(msg + '\r\n'); -} - -/** Log Levels **/ -export namespace Log { - export const system = 0; - export const normal = 1; - export const verbose = 2; -} \ No newline at end of file diff --git a/src/installer/lib/system/options.ts b/src/installer/lib/system/options.ts deleted file mode 100644 index 4aa3adf..0000000 --- a/src/installer/lib/system/options.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Log } from './logger'; -import { OptionsError } from './errors'; -import { getKeys, pick } from './helpers'; -import { getGlobalTSDir } from '../file-utils'; - - -/* ******************************************************************************************************************** - * Options & Type - * ********************************************************************************************************************/ - -export type TSPOptions = typeof defaultOptions & { - /** @deprecated Use 'dir' */ - basedir?: string -} - -export const defaultOptions = { - logLevel: Log.normal, - color: true, - silent: false, - verbose: false, - dir: process.cwd(), - instanceIsCLI: false -}; - -/** App-wide options storage */ -export let appOptions = { ...defaultOptions }; - - -/* ******************************************************************************************************************** - * Parser - * ********************************************************************************************************************/ - -/** - * Create full options object using user input and assigns it to appOptions - */ -export const parseOptions = (options?: Partial): TSPOptions => { - if (!options || (options === appOptions)) return appOptions; - const has = (key: string) => options.hasOwnProperty(key); - - if (has('color')) appOptions.color = options['color']!; - - if (has('basedir')) { - console.warn(`--basedir is deprecated and will be removed in the future. Use --dir instead.`) - options.dir = options.dir || options.basedir; - } - - if (has('persist')) console.warn(`--persist has been removed. Please use prepare script instead!`); - - if (has('global') && has('dir')) throw new OptionsError(`Cannot specify both --global and --dir`); - if (has('global')) options.dir = getGlobalTSDir(); - - Object.assign(appOptions, pick(options, ...getKeys(defaultOptions))); - - appOptions.logLevel = - (appOptions.silent) ? Log.system : - (appOptions.verbose) ? Log.verbose : - (appOptions.instanceIsCLI) ? Log.normal : - defaultOptions.logLevel; - - return appOptions; -}; - -export const resetOptions = (options?: Partial) => parseOptions({ ...defaultOptions, ...options }); diff --git a/src/patch/lib/main.ts b/src/patch/lib/main.ts deleted file mode 100644 index a3733a4..0000000 --- a/src/patch/lib/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -namespace tsp { - Object.defineProperties(ts, { - isTSC: { get: () => tsp.isTSC, enumerable: true }, - tspVersion: { get: () => tsp.tspVersion, enumerable: true }, - PluginCreator: { get: () => tsp.PluginCreator, enumerable: true }, - originalCreateProgram: { value: ts.createProgram, enumerable: true }, - createProgram: { value: tsp.createProgram }, - diagnosticMap: { get: () => tsp.diagnosticMap, enumerable: true } - }); -} diff --git a/src/patch/lib/plugin.ts b/src/patch/lib/plugin.ts deleted file mode 100644 index f1523de..0000000 --- a/src/patch/lib/plugin.ts +++ /dev/null @@ -1,247 +0,0 @@ -/* - * The logic in this file is based on TTypescript (https://github.com/cevek/ttypescript) - * Credit & thanks go to cevek (https://github.com/cevek) for the incredible work! - */ - -namespace tsp { - const path = require('path'); - const fs = require('fs'); - - let tsNodeIncluded = false; - const requireStack: string[] = []; - - /* ********************************************************* * - * PluginCreator (Class) - * ********************************************************* */ - - /** - * @example - * - * new PluginCreator([ - * {transform: '@zerollup/ts-transform-paths', someOption: '123'}, - * {transform: '@zerollup/ts-transform-paths', type: 'ls', someOption: '123'}, - * {transform: '@zerollup/ts-transform-paths', type: 'ls', after: true, someOption: '123'} - * ]).createTransformers({ program }) - */ - export class PluginCreator { - currentProject?: string - - constructor( - private configs: PluginConfig[], - private resolveBaseDir: string = process.cwd() - ) { - PluginCreator.validateConfigs(configs); - - // Support for deprecated 1.1 name - for (const config of configs) if (config['beforeEmit']) config.transformProgram = true; - } - - public mergeTransformers(into: TransformerList, source: ts.CustomTransformers | TransformerBasePlugin) { - const slice = (input: T | T[]) => (Array.isArray(input) ? input.slice() : [ input ]); - - if (source.before) into.before.push(...slice(source.before)); - if (source.after) into.after.push(...slice(source.after)); - if (source.afterDeclarations) into.afterDeclarations.push(...slice(source.afterDeclarations)); - - return this; - } - - public createTransformers( - params: { program: ts.Program } | { ls: ts.LanguageService }, - customTransformers?: ts.CustomTransformers - ): TransformerList { - const transformers: TransformerList = { before: [], after: [], afterDeclarations: [] }; - - const [ ls, program ] = ('ls' in params) ? [ params.ls, params.ls.getProgram()! ] : [ void 0, params.program ]; - - for (const config of this.configs) { - if (!config.transform || config.transformProgram) continue; - - const factory = this.resolveFactory(config); - if (factory === undefined) continue; - - this.mergeTransformers( - transformers, - PluginCreator.createTransformerFromPattern({ factory: factory, config, program, ls }) - ); - } - - // Chain custom transformers at the end - if (customTransformers) this.mergeTransformers(transformers, customTransformers); - - return transformers; - } - - public getProgramTransformers(): [ ProgramTransformer, PluginConfig ][] { - const res: [ ProgramTransformer, PluginConfig ][] = []; - for (const config of this.configs) { - if (!config.transform || !config.transformProgram) continue; - - const factory = this.resolveFactory(config) as ProgramTransformer; - if (factory === undefined) continue; - - res.push([ factory, config ]); - } - - return res; - } - - /* *********************************************************** - * Helpers - * ***********************************************************/ - - private resolveFactory(config: PluginConfig): PluginFactory | ProgramTransformer | undefined { - const tsConfig = config.tsConfig && path.resolve(this.resolveBaseDir, config.tsConfig); - const transform = config.transform!; - const importKey = config.import || 'default'; - - /* Add support for TS transformers */ - let tsConfigCleanup: Function | undefined; - if (transform!.match(/\.ts$/)) { - // If tsConfig is specified and it differs, we need to re-register tsNode - if (tsNodeIncluded && (tsConfig !== this.currentProject)) tsNodeIncluded = false; - this.currentProject = tsConfig; - - if (!tsNodeIncluded) { - /* Register tsNode */ - try { - require('ts-node').register({ - transpileOnly: true, - ...(tsConfig ? { project: tsConfig } : { skipProject: true }), - compilerOptions: { - target: 'ES2018', - jsx: 'react', - esModuleInterop: true, - module: 'commonjs', - } - }); - tsNodeIncluded = true; - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') - throw new Error( - `Cannot use a typescript-based transformer without ts-node installed. `+ - `Either add ts-node as a (dev)-dependency or install globally.` - ); - else throw e; - } - } - - /* Try to add path mapping support, if paths specified */ - const tsConfigData = tsConfig && ts.readConfigFile(tsConfig, ts.sys.readFile).config; - if (tsConfigData?.compilerOptions?.paths) { - try { - const tsConfigPaths = require('tsconfig-paths'); - const { absoluteBaseUrl } = tsConfigPaths.loadConfig(tsConfig); - tsConfigCleanup = tsConfigPaths.register({ - baseUrl: absoluteBaseUrl, - paths: tsConfigData.compilerOptions.paths - }); - } - catch (e) { - if (e.code === 'MODULE_NOT_FOUND') - console.warn( - `Paths specified in transformer tsconfig.json, but they may not be able to be resolved. ` + - `If not, try adding 'tsconfig-paths' as a dev dependency` - ); - else throw e; - } - } - } - - const modulePath = require.resolve(transform, { paths: [ this.resolveBaseDir ]}); - - /* Prevent recursive requiring of createTransformers (issue with ts-node) */ - if (requireStack.indexOf(modulePath) > -1) return; - - /* Load plugin */ - requireStack.push(modulePath); - const commonjsModule: PluginFactory | { [key: string]: PluginFactory } = require(modulePath); - requireStack.pop(); - - // Un-register path mapping if in place - tsConfigCleanup?.(); - - const factoryModule = (typeof commonjsModule === 'function') ? { default: commonjsModule } : commonjsModule; - const factory = factoryModule[importKey]; - - if (!factory) - throw new Error( - `tsconfig.json > plugins: "${transform}" does not have an export "${importKey}": ` + - require('util').inspect(factoryModule) - ); - - if (typeof factory !== 'function') { - throw new Error( - `tsconfig.json > plugins: "${transform}" export "${importKey}" is not a plugin: ` + - require('util').inspect(factory) - ); - } - - return factory; - } - - static validateConfigs(configs: PluginConfig[]) { - for (const config of configs) - if (!config.name && !config.transform) throw new Error('tsconfig.json plugins error: transform must be present'); - } - - static createTransformerFromPattern({ factory, config, program, ls }: { - factory: PluginFactory; - config: PluginConfig; - program: ts.Program; - ls?: ts.LanguageService; - }): TransformerBasePlugin - { - const { transform, after, afterDeclarations, name, type, transformProgram, ...cleanConfig } = config; - - if (!transform) throw new Error('Not a valid config entry: "transform" key not found'); - - let ret: TransformerPlugin; - switch (config.type) { - case 'ls': - if (!ls) throw new Error(`Plugin ${transform} needs a LanguageService`); - ret = (factory as LSPattern)(ls, cleanConfig); - break; - - case 'config': - ret = (factory as ConfigPattern)(cleanConfig); - break; - - case 'compilerOptions': - ret = (factory as CompilerOptionsPattern)(program.getCompilerOptions(), cleanConfig); - break; - - case 'checker': - ret = (factory as TypeCheckerPattern)(program.getTypeChecker(), cleanConfig); - break; - - case undefined: - case 'program': - const { addDiagnostic, removeDiagnostic, diagnostics } = diagnosticExtrasFactory(program); - - ret = (factory as ProgramPattern)(program, cleanConfig, { - ts: ts, - addDiagnostic, - removeDiagnostic, - diagnostics, - library: getCurrentLibrary() - }); - break; - - case 'raw': - ret = (ctx: ts.TransformationContext) => (factory as RawPattern)(ctx, program, cleanConfig); - break; - - default: - throw new Error(`Invalid plugin type found in tsconfig.json: '${config.type}'`); - } - - if (typeof ret === 'function') - return after ? ({ after: ret }) : - afterDeclarations ? ({ afterDeclarations: ret as ts.TransformerFactory }) : - { before: ret }; - - return ret; - } - } -} diff --git a/src/patch/lib/shared.ts b/src/patch/lib/shared.ts deleted file mode 100644 index dda9d7b..0000000 --- a/src/patch/lib/shared.ts +++ /dev/null @@ -1,26 +0,0 @@ -namespace tsp { - /* ********************************************************* * - * Vars - * ********************************************************* */ - - export const diagnosticMap: typeof ts.diagnosticMap = new WeakMap(); - export declare let isTSC: boolean; - export declare let tspVersion: string; - - /* ********************************************************* * - * Utils - * ********************************************************* */ - - /** @internal */ - export function diagnosticExtrasFactory(program: ts.Program) { - const diagnostics = diagnosticMap.get(program) || diagnosticMap.set(program, []).get(program)!; - - const addDiagnostic = (diag: ts.Diagnostic): number => diagnostics.push(diag); - const removeDiagnostic = (index: number) => { diagnostics.splice(index, 1) }; - - return { addDiagnostic, removeDiagnostic, diagnostics }; - } - - /** @internal */ - export const getCurrentLibrary = () => require('path').basename(__filename, require('path').extname(__filename)); -} diff --git a/src/patch/lib/types/typescript.ts b/src/patch/lib/types/typescript.ts deleted file mode 100644 index 32e2304..0000000 --- a/src/patch/lib/types/typescript.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* ****************************************************************************************************************** * - * Internal - * ****************************************************************************************************************** */ - -/** @internal */ -declare namespace ts { - export type Program = import('typescript').Program; - export type TypeChecker = import('typescript').TypeChecker; - export type Transformer = import('typescript').Transformer; - export type CompilerOptions = import('typescript').CompilerOptions; - export type Diagnostic = import('typescript').Diagnostic; - export type CreateProgramOptions = import('typescript').CreateProgramOptions; - export type CompilerHost = import('typescript').CompilerHost; - export type WriteFileCallback= import('typescript').WriteFileCallback; - export type CancellationToken = import('typescript').CancellationToken; - export type EmitResult = import('typescript').EmitResult; - export type CustomTransformers = import('typescript').CustomTransformers; - export type LanguageService = import('typescript').LanguageService; - export type TransformerFactory = import('typescript').TransformerFactory; - export type TransformationContext = import('typescript').TransformationContext; - export type SourceFile = import('typescript').SourceFile; - export type Bundle = import('typescript').Bundle; - - export const createProgram: typeof import('typescript').createProgram; - export const findConfigFile: typeof import('typescript').findConfigFile; - export const readConfigFile: typeof import('typescript').readConfigFile; - export const parseJsonConfigFileContent: typeof import('typescript').parseJsonConfigFileContent; - export const sys: typeof import('typescript').sys; -} - - -/* ****************************************************************************************************************** * - * Added Properties - * ****************************************************************************************************************** */ - -/** @build-types */ -declare namespace ts { - export const tspVersion: string; - export const originalCreateProgram: typeof ts.createProgram; - export const diagnosticMap: WeakMap; -} diff --git a/src/tsconfig.json b/src/tsconfig.json deleted file mode 100644 index ba7673a..0000000 --- a/src/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": [ "installer", "shared" ], - - "compilerOptions": { - "rootDirs": [ - "installer", - "shared" - ], - "outDir": "../dist", - "sourceMap": true, - "composite": true, - "declaration": true, - - "plugins": [ - { - "transform": "./plugin.ts", - "transformProgram": true - } - ] - } -} diff --git a/test/assets/projects/main/package.json b/test/assets/projects/main/package.json new file mode 100644 index 0000000..01959ad --- /dev/null +++ b/test/assets/projects/main/package.json @@ -0,0 +1,5 @@ +{ + "name": "test-project", + "private": true, + "dependencies": {} +} diff --git a/test/assets/projects/main/src/index.ts b/test/assets/projects/main/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/assets/projects/main/tsconfig.json b/test/assets/projects/main/tsconfig.json new file mode 100644 index 0000000..238b6ed --- /dev/null +++ b/test/assets/projects/main/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include" : [ "src" ], + "compilerOptions": { + "outDir": "dist", + "esModuleInterop": true, + "strict": true, + + "lib": [ "ESNext" ], + "target": "ESNext", + "module": "CommonJS", + "moduleResolution": "node", + + "newLine": "LF", + "allowSyntheticDefaultImports": true + } +} diff --git a/test/assets/projects/path-mapping/base.ts b/test/assets/projects/path-mapping/base.ts new file mode 100644 index 0000000..2021030 --- /dev/null +++ b/test/assets/projects/path-mapping/base.ts @@ -0,0 +1 @@ +export const b = 2; diff --git a/test/assets/projects/path-mapping/package.json b/test/assets/projects/path-mapping/package.json new file mode 100644 index 0000000..49d7651 --- /dev/null +++ b/test/assets/projects/path-mapping/package.json @@ -0,0 +1,8 @@ +{ + "name": "path-mapping-test", + "main": "src/index.ts", + "dependencies": { + "ts-node" : "^10.9.1", + "tsconfig-paths" : "^4.2.0" + } +} diff --git a/test/assets/projects/path-mapping/src/a/a.ts b/test/assets/projects/path-mapping/src/a/a.ts new file mode 100644 index 0000000..1f60d0c --- /dev/null +++ b/test/assets/projects/path-mapping/src/a/a.ts @@ -0,0 +1 @@ +export const aVar = "a"; diff --git a/test/assets/projects/path-mapping/src/b.ts b/test/assets/projects/path-mapping/src/b.ts new file mode 100644 index 0000000..454fdb8 --- /dev/null +++ b/test/assets/projects/path-mapping/src/b.ts @@ -0,0 +1 @@ +export const bVar = "b"; diff --git a/test/assets/projects/path-mapping/src/index.ts b/test/assets/projects/path-mapping/src/index.ts new file mode 100644 index 0000000..9402b71 --- /dev/null +++ b/test/assets/projects/path-mapping/src/index.ts @@ -0,0 +1,29 @@ +// @ts-nocheck + +import * as ts from 'typescript'; +import '@a/a'; +import '@b'; + +const tryRequire = (path: string) => { + try { + require(path); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + return false; + } else { + throw e; + } + } + + return true; +} + +export default function(program: ts.Program, pluginOptions: any) { + return (ctx: ts.TransformationContext) => { + process.stdout.write(`sub-path:${tryRequire('@a/a')}\n`); + process.stdout.write(`path:${tryRequire('@b')}\n`); + process.stdout.write(`non-mapped:${tryRequire('@c')}\n`); + + return (_: any) => _; + }; +} diff --git a/test/assets/projects/path-mapping/tsconfig.json b/test/assets/projects/path-mapping/tsconfig.json new file mode 100644 index 0000000..7b8f8b1 --- /dev/null +++ b/test/assets/projects/path-mapping/tsconfig.json @@ -0,0 +1,17 @@ +{ + "include": [ "base.ts" ], + "compilerOptions": { + "outDir" : "dist", + "moduleResolution" : "node", + "module": "commonjs", + "target": "ES2020", + "noEmit": false, + "plugins" : [ + { + "transform": "./src/index.ts", + "tsConfig": "./tsconfig.plugin.json", + "resolvePathAliases": "always", + } + ] + } +} diff --git a/test/assets/projects/path-mapping/tsconfig.plugin.json b/test/assets/projects/path-mapping/tsconfig.plugin.json new file mode 100644 index 0000000..2773420 --- /dev/null +++ b/test/assets/projects/path-mapping/tsconfig.plugin.json @@ -0,0 +1,14 @@ +{ + "include": [ "src" ], + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "target": "ES2020", + "noEmit": true, + "baseUrl" : "src", + "paths": { + "@a/*": [ "./a/*" ], + "@b": [ "./b" ], + } + } +} diff --git a/test/assets/projects/transform/package.json b/test/assets/projects/transform/package.json new file mode 100644 index 0000000..19a8411 --- /dev/null +++ b/test/assets/projects/transform/package.json @@ -0,0 +1,8 @@ +{ + "name": "minimal-ts-patch", + "main": "src/index.ts", + "dependencies": { + "esm": "^3.2.25", + "ts-node" : "^10.9.1" + } +} diff --git a/test/assets/projects/transform/run-transform.js b/test/assets/projects/transform/run-transform.js new file mode 100644 index 0000000..7d432e7 --- /dev/null +++ b/test/assets/projects/transform/run-transform.js @@ -0,0 +1,41 @@ +const path = require('path'); + + +/* ****************************************************************************************************************** * + * Helpers + * ****************************************************************************************************************** */ + +function getTransformedFile(transformerKind) { + const tsInstance = require('ts-patch/compiler'); + + + const configPath = path.join(__dirname, `tsconfig.${transformerKind}.json`); + const configText = tsInstance.sys.readFile(configPath); + const configParseResult = tsInstance.parseConfigFileTextToJson(configPath, configText); + const config = configParseResult.config; + + config.compilerOptions.noEmit = false; + config.compilerOptions.skipLibCheck = true; + config.compilerOptions.outDir = 'dist'; + + const emittedFiles = new Map(); + + const writeFile = (fileName, content) => emittedFiles.set(fileName, content); + + const program = tsInstance.createProgram({ + rootNames: [ path.join(__dirname, 'src', 'index.ts') ], + options: config.compilerOptions, + }); + + program.emit(undefined, writeFile); + + return emittedFiles.get('dist/index.js'); +} + + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +const args = process.argv.slice(2); +console.log(getTransformedFile(args[0])); diff --git a/test/assets/projects/transform/src/index.ts b/test/assets/projects/transform/src/index.ts new file mode 100644 index 0000000..a8d763d --- /dev/null +++ b/test/assets/projects/transform/src/index.ts @@ -0,0 +1,4 @@ +const a = 'before'; + +// Intentional error — should be ignored +declare const x: string = 3; diff --git a/test/assets/projects/transform/transformers/cjs/js-plugin.cjs b/test/assets/projects/transform/transformers/cjs/js-plugin.cjs new file mode 100644 index 0000000..3b33d88 --- /dev/null +++ b/test/assets/projects/transform/transformers/cjs/js-plugin.cjs @@ -0,0 +1,19 @@ +const ts = require('typescript'); + +if (!__dirname) throw new Error('Not handled as cjs'); + +module.exports.default = (program, _, { ts: tsInstance }) => { + return (ctx) => { + const factory = ctx.factory; + + return (sourceFile) => { + function visit(node) { + if (tsInstance.isStringLiteral(node) && node.text === 'before') { + return factory.createStringLiteral('after-cjs'); + } + return tsInstance.visitEachChild(node, visit, ctx); + } + return tsInstance.visitNode(sourceFile, visit); + }; + }; +} diff --git a/test/assets/projects/transform/transformers/cjs/package.json b/test/assets/projects/transform/transformers/cjs/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/test/assets/projects/transform/transformers/cjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/assets/projects/transform/transformers/cjs/plugin.cts b/test/assets/projects/transform/transformers/cjs/plugin.cts new file mode 100644 index 0000000..7808fac --- /dev/null +++ b/test/assets/projects/transform/transformers/cjs/plugin.cts @@ -0,0 +1,17 @@ +import type * as ts from 'typescript'; + +export default function (program: ts.Program, _, { ts: tsInstance }: { ts: typeof ts }) { + return (ctx: ts.TransformationContext) => { + const factory = ctx.factory; + + return (sourceFile: ts.SourceFile) => { + function visit(node: ts.Node): ts.Node { + if (tsInstance.isStringLiteral(node) && node.text === 'before') { + return factory.createStringLiteral('after-cts'); + } + return tsInstance.visitEachChild(node, visit, ctx); + } + return tsInstance.visitNode(sourceFile, visit); + }; + }; +} diff --git a/test/assets/projects/transform/transformers/cjs/tsconfig.json b/test/assets/projects/transform/transformers/cjs/tsconfig.json new file mode 100644 index 0000000..4ae34e6 --- /dev/null +++ b/test/assets/projects/transform/transformers/cjs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends" : "../../tsconfig.json", + "include" : [ "." ], + "compilerOptions" : { + "module" : "CommonJS" + } +} diff --git a/test/assets/projects/transform/transformers/esm/js-plugin.mjs b/test/assets/projects/transform/transformers/esm/js-plugin.mjs new file mode 100644 index 0000000..d3bc8e4 --- /dev/null +++ b/test/assets/projects/transform/transformers/esm/js-plugin.mjs @@ -0,0 +1,17 @@ +if (!import.meta.url) throw new Error('Not handled as esm'); + +export default function (program, _, { ts: tsInstance }) { + return (ctx) => { + const factory = ctx.factory; + + return (sourceFile) => { + function visit(node) { + if (tsInstance.isStringLiteral(node) && node.text === 'before') { + return factory.createStringLiteral('after-mjs'); + } + return tsInstance.visitEachChild(node, visit, ctx); + } + return tsInstance.visitNode(sourceFile, visit); + }; + }; +} diff --git a/test/assets/projects/transform/transformers/esm/package.json b/test/assets/projects/transform/transformers/esm/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/test/assets/projects/transform/transformers/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/assets/projects/transform/transformers/esm/plugin.mts b/test/assets/projects/transform/transformers/esm/plugin.mts new file mode 100644 index 0000000..faeadbf --- /dev/null +++ b/test/assets/projects/transform/transformers/esm/plugin.mts @@ -0,0 +1,19 @@ +import type * as ts from 'typescript'; + +if (!import.meta.url) throw new Error('Not handled as esm'); + +export default function (program: ts.Program, _, { ts: tsInstance }: { ts: typeof ts }) { + return (ctx: ts.TransformationContext) => { + const factory = ctx.factory; + + return (sourceFile: ts.SourceFile) => { + function visit(node: ts.Node): ts.Node { + if (tsInstance.isStringLiteral(node) && node.text === 'before') { + return factory.createStringLiteral('after-mts'); + } + return tsInstance.visitEachChild(node, visit, ctx); + } + return tsInstance.visitNode(sourceFile, visit); + }; + }; +} diff --git a/test/assets/projects/transform/transformers/esm/plugin.ts b/test/assets/projects/transform/transformers/esm/plugin.ts new file mode 100644 index 0000000..eced594 --- /dev/null +++ b/test/assets/projects/transform/transformers/esm/plugin.ts @@ -0,0 +1,19 @@ +import type * as ts from 'typescript'; + +if (!import.meta.url) throw new Error('Not handled as esm'); + +export default function (program: ts.Program, _, { ts: tsInstance }: { ts: typeof ts }) { + return (ctx: ts.TransformationContext) => { + const factory = ctx.factory; + + return (sourceFile: ts.SourceFile) => { + function visit(node: ts.Node): ts.Node { + if (tsInstance.isStringLiteral(node) && node.text === 'before') { + return factory.createStringLiteral('after-ts'); + } + return tsInstance.visitEachChild(node, visit, ctx); + } + return tsInstance.visitNode(sourceFile, visit); + }; + }; +} diff --git a/test/assets/projects/transform/transformers/esm/tsconfig.json b/test/assets/projects/transform/transformers/esm/tsconfig.json new file mode 100644 index 0000000..047636f --- /dev/null +++ b/test/assets/projects/transform/transformers/esm/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends" : "../../tsconfig.json", + "include" : [ "." ], + "compilerOptions" : { + "module" : "ESNext" + } +} diff --git a/test/assets/projects/transform/tsconfig.cjs.json b/test/assets/projects/transform/tsconfig.cjs.json new file mode 100644 index 0000000..8598e4d --- /dev/null +++ b/test/assets/projects/transform/tsconfig.cjs.json @@ -0,0 +1,12 @@ +{ + "extends" : "./tsconfig", + "compilerOptions" : { + "module" : "CommonJS", + "plugins": [ + { + "transform": "./transformers/cjs/js-plugin.cjs", + "tsConfig": "./transformers/cjs/tsconfig.json" + } + ] + } +} diff --git a/test/assets/projects/transform/tsconfig.cts.json b/test/assets/projects/transform/tsconfig.cts.json new file mode 100644 index 0000000..6b72a72 --- /dev/null +++ b/test/assets/projects/transform/tsconfig.cts.json @@ -0,0 +1,12 @@ +{ + "extends" : "./tsconfig", + "compilerOptions" : { + "module" : "CommonJS", + "plugins": [ + { + "transform": "./transformers/cjs/plugin.cts", + "tsConfig": "./transformers/cjs/tsconfig.json" + } + ] + } +} diff --git a/test/assets/projects/transform/tsconfig.json b/test/assets/projects/transform/tsconfig.json new file mode 100644 index 0000000..a810423 --- /dev/null +++ b/test/assets/projects/transform/tsconfig.json @@ -0,0 +1,9 @@ +{ + "include": [ "src" ], + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "target": "esnext", + "noEmit": true + } +} diff --git a/test/assets/projects/transform/tsconfig.mjs.json b/test/assets/projects/transform/tsconfig.mjs.json new file mode 100644 index 0000000..7495857 --- /dev/null +++ b/test/assets/projects/transform/tsconfig.mjs.json @@ -0,0 +1,12 @@ +{ + "extends" : "./tsconfig", + "compilerOptions" : { + "module" : "CommonJS", + "plugins": [ + { + "transform": "./transformers/esm/js-plugin.mjs", + "tsConfig": "./transformers/esm/tsconfig.json" + } + ] + } +} diff --git a/test/assets/projects/transform/tsconfig.mts.json b/test/assets/projects/transform/tsconfig.mts.json new file mode 100644 index 0000000..711b87f --- /dev/null +++ b/test/assets/projects/transform/tsconfig.mts.json @@ -0,0 +1,11 @@ +{ + "extends" : "./tsconfig", + "compilerOptions" : { + "plugins": [ + { + "transform": "./transformers/esm/plugin.mts", + "tsConfig": "./transformers/esm/tsconfig.json" + } + ] + } +} diff --git a/test/assets/projects/transform/tsconfig.ts.json b/test/assets/projects/transform/tsconfig.ts.json new file mode 100644 index 0000000..7c6c090 --- /dev/null +++ b/test/assets/projects/transform/tsconfig.ts.json @@ -0,0 +1,11 @@ +{ + "extends" : "./tsconfig", + "compilerOptions" : { + "plugins": [ + { + "transform": "./transformers/esm/plugin.ts", + "tsConfig": "./transformers/esm/tsconfig.json" + } + ] + } +} diff --git a/test/assets/projects/webpack/esm-plugin.mjs b/test/assets/projects/webpack/esm-plugin.mjs new file mode 100644 index 0000000..f550706 --- /dev/null +++ b/test/assets/projects/webpack/esm-plugin.mjs @@ -0,0 +1,5 @@ +export default function(program, pluginOptions) { + return (ctx) => { + throw new Error(`ts-patch worked (esm)`); + }; +} diff --git a/test/assets/projects/webpack/esm-plugin.mts b/test/assets/projects/webpack/esm-plugin.mts new file mode 100644 index 0000000..6b4137b --- /dev/null +++ b/test/assets/projects/webpack/esm-plugin.mts @@ -0,0 +1,5 @@ +export default function(program: any, pluginOptions: any) { + return (ctx) => { + throw new Error(`ts-patch worked (esmts)`); + }; +} diff --git a/test/assets/projects/webpack/hide-module.js b/test/assets/projects/webpack/hide-module.js new file mode 100644 index 0000000..8e0b604 --- /dev/null +++ b/test/assets/projects/webpack/hide-module.js @@ -0,0 +1,24 @@ +const Module = require('module'); + + +/* ****************************************************************************************************************** * + * Config + * ****************************************************************************************************************** */ + +const hiddenModules = (process.env.HIDE_MODULES || '').split(',').map(str => str.trim()); + + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +const originalRequire = Module.prototype.require; +Module.prototype.require = function(requestedModule) { + if (hiddenModules.includes(requestedModule)) { + const error = new Error(`Cannot find module '${requestedModule}'`); + error.code = 'MODULE_NOT_FOUND'; + throw error; + } + + return originalRequire.call(this, requestedModule); +}; diff --git a/test/assets/projects/webpack/package.json b/test/assets/projects/webpack/package.json new file mode 100644 index 0000000..ad637d0 --- /dev/null +++ b/test/assets/projects/webpack/package.json @@ -0,0 +1,14 @@ +{ + "name": "webpack-tspatch-project", + "version": "1.0.0", + "scripts": { + "build": "webpack" + }, + "devDependencies" : { + "ts-loader": "^9.4.2", + "webpack": "^5.79.0", + "webpack-cli": "^5.0.1", + "esm": "^3.2.25", + "ts-node" : "^10.9.1" + } +} diff --git a/test/assets/projects/webpack/plugin.ts b/test/assets/projects/webpack/plugin.ts new file mode 100644 index 0000000..27a3014 --- /dev/null +++ b/test/assets/projects/webpack/plugin.ts @@ -0,0 +1,7 @@ +import type * as ts from 'typescript'; + +export default function(program: ts.Program, pluginOptions: any) { + return (ctx: ts.TransformationContext) => { + throw new Error(`ts-patch worked (cjs)`); + }; +} diff --git a/test/assets/projects/webpack/src/index.ts b/test/assets/projects/webpack/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/assets/projects/webpack/tsconfig.esm.json b/test/assets/projects/webpack/tsconfig.esm.json new file mode 100644 index 0000000..69f4788 --- /dev/null +++ b/test/assets/projects/webpack/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "plugins": [ + { "transform": "./esm-plugin.mjs" } + ] + } +} diff --git a/test/assets/projects/webpack/tsconfig.esmts.json b/test/assets/projects/webpack/tsconfig.esmts.json new file mode 100644 index 0000000..db4583d --- /dev/null +++ b/test/assets/projects/webpack/tsconfig.esmts.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "plugins": [ + { "transform": "./esm-plugin.mts" } + ] + } +} diff --git a/test/assets/projects/webpack/tsconfig.json b/test/assets/projects/webpack/tsconfig.json new file mode 100644 index 0000000..0f4aca5 --- /dev/null +++ b/test/assets/projects/webpack/tsconfig.json @@ -0,0 +1,13 @@ +{ + "include": [ "src" ], + "compilerOptions": { + "module": "commonjs", + "noEmit": true, + "target": "ESNext", + "declaration": false, + "moduleResolution" : "Node", + "plugins": [ + { "transform": "./plugin.ts" } + ] + } +} diff --git a/test/assets/projects/webpack/webpack.config.js b/test/assets/projects/webpack/webpack.config.js new file mode 100644 index 0000000..97f9899 --- /dev/null +++ b/test/assets/projects/webpack/webpack.config.js @@ -0,0 +1,24 @@ +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/index.ts', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'bundle.js' + }, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + loader: require.resolve('ts-loader'), + options: { + compiler: 'ts-patch/typescript' + } + } + ] + }, + resolve: { + extensions: ['.ts', '.tsx', '.js'] + } +}; diff --git a/test/assets/src-files/mapping-project/index.ts b/test/assets/src-files/mapping-project/index.ts deleted file mode 100644 index e921523..0000000 --- a/test/assets/src-files/mapping-project/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('hello'); diff --git a/test/assets/src-files/mapping-project/transformer/a.ts b/test/assets/src-files/mapping-project/transformer/a.ts deleted file mode 100644 index c269f24..0000000 --- a/test/assets/src-files/mapping-project/transformer/a.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function transformer() { - console.log('Path-Mapping Success!'); - return () => (node:any) => node -} diff --git a/test/assets/src-files/mapping-project/transformer/transformer.ts b/test/assets/src-files/mapping-project/transformer/transformer.ts deleted file mode 100644 index e832f96..0000000 --- a/test/assets/src-files/mapping-project/transformer/transformer.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { transformer } from '#a'; -export { transformer }; diff --git a/test/assets/src-files/mapping-project/transformer/tsconfig.json b/test/assets/src-files/mapping-project/transformer/tsconfig.json deleted file mode 100644 index 24cb4c7..0000000 --- a/test/assets/src-files/mapping-project/transformer/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "include": [ "." ], - "compilerOptions": { - "noEmit": true, - - "baseUrl": ".", - "paths": { - "#a": [ "./a.ts" ] - } - } -} diff --git a/test/assets/src-files/mapping-project/tsconfig.json b/test/assets/src-files/mapping-project/tsconfig.json deleted file mode 100644 index 9cda48e..0000000 --- a/test/assets/src-files/mapping-project/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "files": [ "index.ts" ], - "compilerOptions": { - "noEmit": true, - "plugins": [ { - "transform": "./transformer/transformer.ts", - "tsConfig": "./transformer/tsconfig.json", - "import": "transformer" - } ], - "skipDefaultLibCheck": true, - "skipLibCheck": true - } -} diff --git a/test/assets/src-files/mapping-project/tsconfig.noproject.json b/test/assets/src-files/mapping-project/tsconfig.noproject.json deleted file mode 100644 index 0a930d1..0000000 --- a/test/assets/src-files/mapping-project/tsconfig.noproject.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "plugins": [ { - "transform": "./transformer/transformer.ts", - "import": "transformer" - } ], - "skipDefaultLibCheck": true, - "skipLibCheck": true - } -} diff --git a/test/assets/src-files/safely-code.ts b/test/assets/src-files/safely-code.ts deleted file mode 100644 index f6ce5fb..0000000 --- a/test/assets/src-files/safely-code.ts +++ /dev/null @@ -1,7 +0,0 @@ -const a = { b: 1 }; -declare function safely(a: any): void; - -function abc() { - const c = safely(a.b); -} -console.log(abc.toString()); diff --git a/test/assets/src-files/tsconfig.alter-diags.json b/test/assets/src-files/tsconfig.alter-diags.json deleted file mode 100644 index 11303af..0000000 --- a/test/assets/src-files/tsconfig.alter-diags.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files": [ "tsnode-code.ts" ], - "compilerOptions": { - "target": "es5", - "lib": ["es2015"], - "module": "commonjs", - "typeRoots": [ ], - "plugins": [{ "transform": "../transformers/alter-diagnostics.ts" }], - "noEmit": true - } -} diff --git a/test/assets/src-files/tsconfig.json b/test/assets/src-files/tsconfig.json deleted file mode 100644 index 353d739..0000000 --- a/test/assets/src-files/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": [ "ES2015" ], - "module": "commonjs", - "noEmit": true, - "typeRoots": [ ], - "plugins": [{ "transform": "../transformers/before.ts" }] - } -} diff --git a/test/assets/src-files/tsnode-code.ts b/test/assets/src-files/tsnode-code.ts deleted file mode 100644 index ed4885c..0000000 --- a/test/assets/src-files/tsnode-code.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const console: any -type T2 = string['bad'] - -function type() { - return ''; -} -const x = type<{ abc: 1 }>(); -console.log(x); diff --git a/test/assets/test-package.json b/test/assets/test-package.json deleted file mode 100644 index abd4c2b..0000000 --- a/test/assets/test-package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "test-project", - "private": true, - "dependencies": { - "ts-patch": "*", - "typescript": "*", - } -} diff --git a/test/assets/transformers/alter-diagnostics.ts b/test/assets/transformers/alter-diagnostics.ts deleted file mode 100644 index 0c0503b..0000000 --- a/test/assets/transformers/alter-diagnostics.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as TS from 'typescript'; -import { DiagnosticCategory, DiagnosticWithLocation, Diagnostic } from 'typescript'; -import type { TransformerExtras } from '../../../src/shared/plugin-types'; - -export default function ( - program: TS.Program, - opts: any, - extras: TransformerExtras -) { - const { addDiagnostic, diagnostics, removeDiagnostic, library, ts } = extras; - - const diagPassed = !!diagnostics; - let diags = diagnostics.slice(); - - // Non-tsc doesn't get a pre-emit diagnostic array - // And older TS versions don't get semantic diagnostics until after emit is called - if ((library !== 'tsc') || ((ts).versionMajorMinor === '2.7')) - diags = diags.concat(program.getSemanticDiagnostics()); - - let foundError = false; - diags.forEach((d, i) => { - if (d.code === 2339) { - foundError = true; - removeDiagnostic(i); - } - }); - - const msg = `DIAG_PASSED=${diagPassed} FOUND_ERROR=${foundError} LIBRARY=${library}`; - - addDiagnostic({ - code: 1337, - messageText: msg, - category: DiagnosticCategory.Error, - file: program.getSourceFiles().find(s => /\/tsnode-code.ts$/.test(s.fileName)), - start: 1, - length: 18 - }); - - return (ctx: TS.TransformationContext) => (sourceFile: TS.SourceFile) => - ts.visitNode(sourceFile, (node) => ts.createSourceFile( - sourceFile.fileName, - `// ${msg}`, - sourceFile.languageVersion - )) -} diff --git a/test/assets/transformers/before.ts b/test/assets/transformers/before.ts deleted file mode 100644 index ae15e88..0000000 --- a/test/assets/transformers/before.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as TS from 'typescript'; - -export default function (program: TS.Program, config: any, { ts }: { ts: typeof TS }) { - const checker = program.getTypeChecker(); - return (ctx: TS.TransformationContext) => (sourceFile: TS.SourceFile) => { - function visitor(node: TS.Node): TS.Node { - if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - node.expression.getText() === 'type' && - node.typeArguments && - node.typeArguments[0] - ) { - const type = checker.getTypeFromTypeNode(node.typeArguments[0]); - return ts.createLiteral(checker.typeToString(type)); - } - return ts.visitEachChild(node, visitor, ctx); - } - return ts.visitEachChild(sourceFile, visitor, ctx); - }; -} diff --git a/test/assets/transformers/program-transformer.ts b/test/assets/transformers/program-transformer.ts deleted file mode 100644 index 04277b0..0000000 --- a/test/assets/transformers/program-transformer.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as ts from 'typescript'; -import * as path from 'path'; -import { ProgramTransformer, ProgramTransformerExtras } from 'ts-patch/src/shared/plugin-types'; - - -export const newFiles = [ 'abc1.ts', 'abc2.ts' ].map(f => (ts as any).normalizePath(path.resolve(__dirname, '../src-files', f))); -const safelyFile = path.join(__dirname, '../src-files/safely-code.ts'); - -const readFileFactory = (originalReadFile: Function) => ({ - readFile: function newReadFile(fileName: string) { - return newFiles.includes(fileName) ? 'export const abc = 3;' : originalReadFile(fileName); - } -}); - -function createProgramTransformer(newFileIndex: number) { - return function (program: ts.Program, host: ts.CompilerHost | undefined, config: any, { ts: tsInstance }: ProgramTransformerExtras) { - if (!host) host = tsInstance.createCompilerHost(program.getCompilerOptions()); - if (host.readFile.name !== 'newReadFile') Object.assign(host, readFileFactory(host.readFile)); - if (tsInstance.sys.readFile.name !== 'newReadFile') Object.assign(tsInstance.sys, readFileFactory(tsInstance.sys.readFile)); - - const newProgram = (tsInstance as any).originalCreateProgram( - /* rootNames */ [ safelyFile, newFiles[newFileIndex] ], - program.getCompilerOptions(), - host, - program - ); - - return newProgram; - } -} - -export const progTransformer1 = createProgramTransformer(0); -export const progTransformer2 = createProgramTransformer(1); - -export let progTsInstance: typeof ts | undefined = undefined; - - -/* ****************************************************************************************************************** * - * Recursion - * ****************************************************************************************************************** */ - -export function recursiveTransformer1(program: ts.Program, host: ts.CompilerHost | undefined, config: any, { ts: tsInstance }: ProgramTransformerExtras) { - progTsInstance = tsInstance; - - const newProgram = (tsInstance as any).createProgram( - /* rootNames */ [ safelyFile, newFiles[0] ], - program.getCompilerOptions(), - host, - program - ); - - return newProgram; -} - -export function recursiveTransformer2(program: ts.Program, host: ts.CompilerHost | undefined, config: any, { ts: tsInstance }: ProgramTransformerExtras) { - progTsInstance = tsInstance; - - const newProgram = (tsInstance as any).createProgram( - /* rootNames */ [ safelyFile, newFiles[1] ], - program.getCompilerOptions(), - host, - program - ); - - return newProgram; -} diff --git a/test/assets/transformers/safely.ts b/test/assets/transformers/safely.ts deleted file mode 100644 index 9efae5b..0000000 --- a/test/assets/transformers/safely.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as ts from 'typescript'; - - -export interface MyPluginOptions { - some?: string -} - -export default function myTransformerPlugin( - program: ts.Program, - opts: MyPluginOptions | undefined, - { ts: tsInstance }: { ts: typeof ts } -) { - return { - before(ctx: ts.TransformationContext) { - return (sourceFile: ts.SourceFile) => { - function visitor(node: ts.Node): ts.Node { - if (tsInstance.isCallExpression(node) && node.expression.getText() === 'safely') { - const target = node.arguments[0] - if (tsInstance.isPropertyAccessExpression(target)) { - return tsInstance.createBinary( - target.expression, - tsInstance.SyntaxKind.AmpersandAmpersandToken, - target - ) - } - } - return tsInstance.visitEachChild(node, visitor, ctx) - } - const srcFile = tsInstance.visitEachChild(sourceFile, visitor, ctx); - return srcFile; - } - } - } -} diff --git a/test/assets/transformers/transform-advanced.ts b/test/assets/transformers/transform-advanced.ts deleted file mode 100644 index 6237af2..0000000 --- a/test/assets/transformers/transform-advanced.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as ts from 'typescript' - -export function advancedTransformer(ctx: ts.TransformationContext) { - return (sourceFile: ts.SourceFile) => { - return sourceFile - } -} - -export default function transform1(program: ts.Program) { - return { - after: advancedTransformer - } -} diff --git a/test/assets/transformers/transform-simple.ts b/test/assets/transformers/transform-simple.ts deleted file mode 100644 index 3ba8fec..0000000 --- a/test/assets/transformers/transform-simple.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as ts from 'typescript' - -export function simpleTransformer(ctx: ts.TransformationContext) { - return (sourceFile: ts.SourceFile) => { - return sourceFile - } -} - -export default function transform1(program: ts.Program) { - return simpleTransformer -} diff --git a/test/assets/transformers/transformers.ts b/test/assets/transformers/transformers.ts deleted file mode 100644 index a005c76..0000000 --- a/test/assets/transformers/transformers.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const transformers = { - before: { - transform: __dirname + '/before.ts', - source: `declare function type(): string; var x = type<{ abc: 1 }>();`, - out: `var x = "{ abc: 1 }"\n` - } -}; diff --git a/test/package.json b/test/package.json index 3e62703..b675e80 100644 --- a/test/package.json +++ b/test/package.json @@ -1,14 +1,14 @@ { "private": true, + "scripts" : { + "perf": "ts-node -T --project tsconfig.json --files ./src/perf.ts" + }, "dependencies": { - "ts-40": "npm:typescript@4.0.*", - "ts-48": "npm:typescript@4.8.*", "ts-latest": "npm:typescript@latest", "ts-node": "latest", "ts-patch": "link:..", "tsconfig-paths": "latest", "fs-extra": "^10.0.0", - "@types/fs-extra": "^9.0.13", - "tsp-old": "npm:ts-patch@1.4.5" + "@types/fs-extra": "^9.0.13" } } diff --git a/test/src/cleanup.ts b/test/src/cleanup.ts new file mode 100644 index 0000000..a0746f4 --- /dev/null +++ b/test/src/cleanup.ts @@ -0,0 +1,10 @@ +import { cleanTemp } from './project'; + + +export default function() { + try { + cleanTemp(); + } catch (e) { + console.error(e); + } +} diff --git a/test/src/config.ts b/test/src/config.ts index 218a669..c1b4c85 100644 --- a/test/src/config.ts +++ b/test/src/config.ts @@ -1,4 +1,5 @@ import path from 'path'; +// @ts-expect-error TODO - tsei import { normalizeSlashes } from 'typescript'; @@ -17,16 +18,18 @@ const getTsModule = (label: string, moduleSpecifier: string) => ({ // region: Config /* ****************************************************************************************************************** */ -export const tmpDir = normalizeSlashes(path.resolve(__dirname, '../.tmp')) export const testRootDir = normalizeSlashes(path.resolve(__dirname, '..')); export const rootDir = normalizeSlashes(path.resolve(__dirname, '../../')); export const resourcesDir = normalizeSlashes(path.resolve(__dirname, '../../dist/resources')); export const assetsDir = normalizeSlashes(path.resolve(__dirname, '../assets')); +export const projectsDir = normalizeSlashes(path.resolve(assetsDir, 'projects')); export const tsModules = [ getTsModule('latest', 'ts-latest'), - getTsModule('4.8', 'ts-48'), - getTsModule('4.0', 'ts-40') ] +export const packageManagers = [ 'npm', 'yarn', 'pnpm', 'yarn3' ]; + +export type PackageManager = typeof packageManagers[number]; + // endregion diff --git a/test/src/perf.ts b/test/src/perf.ts new file mode 100644 index 0000000..69f2dba --- /dev/null +++ b/test/src/perf.ts @@ -0,0 +1,110 @@ +import { getTsPackage, TsPackage } from '../../projects/core/src/ts-package'; +import { getTsModule, GetTsModuleOptions, TsModule } from '../../projects/core/src/module'; +import { getPatchedSource, GetPatchedSourceOptions } from '../../projects/core/src/patch/get-patched-source'; +import child_process from 'child_process'; +import path from 'path'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const tsLatestPath = path.dirname(require.resolve('ts-latest/package.json')); + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +function perf(name: string, opt: any = {}, fn: () => any) { + const start = performance.now(); + const res = fn(); + const end = performance.now(); + console.log(`${name} (${JSON.stringify(opt)}): \n — duration: ${end - start} ms\n`); + return res; +} + +function printOpt(opt: any) { + const printOpt = { ...opt }; + if (printOpt.tsPackage) printOpt.tsPackage = printOpt.tsPackage.packageDir; + if (printOpt.tsModule) printOpt.tsModule = printOpt.tsModule.moduleName; + return printOpt; +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function perfTsPackage(opt: { tsPath?: string } = {}) { + opt.tsPath ??= tsLatestPath; + perf(`tsPackage`, printOpt(opt), () => getTsPackage(opt.tsPath)); +} + +export function perfTsModule(opt: { moduleName?: TsModule.Name, tsPackage?: TsPackage } & GetTsModuleOptions = {}) { + opt.tsPackage ??= getTsPackage(tsLatestPath); + opt.moduleName ??= 'typescript.js'; + perf(`tsModule`, printOpt(opt), () => getTsModule(opt.tsPackage!, opt.moduleName!, opt)); +} + +export function perfGetPatchedSource(opt: { tsModule?: TsModule } & GetPatchedSourceOptions = {}) { + opt.tsModule ??= getTsModule(getTsPackage(tsLatestPath), 'typescript.js'); + perf(`getPatchedSource`, printOpt(opt), () => getPatchedSource(opt.tsModule!, opt)); +} + +export function perfTspc(opt: { skipCache?: boolean } = {}) { + opt.skipCache ??= false; + perf( + `tspc`, + printOpt(opt), + () => { + // Execute tspc command with node in a child process + child_process.execSync(`node ${path.resolve(__dirname, '../../dist/bin/tspc.js --help')}`, { + env: { + ...process.env, + TSP_SKIP_CACHE: opt.skipCache ? 'true' : 'false', + TSP_COMPILER_TS_PATH: tsLatestPath, + } + }); + } + ); +} + +export function perfTsc() { + perf( + `tsc`, + {}, + () => { + child_process.execSync(`node ${path.join(tsLatestPath, 'lib/tsc.js')} --help`, { + env: { + ...process.env, + TSP_COMPILER_TS_PATH: tsLatestPath, + } + }); + } + ); +} + +// TODO - Add perfInstall with and without cache + +export function perfAll() { + perfTsPackage(); + perfTsModule(); + perfGetPatchedSource(); + perfTsc(); + perfTspc({ skipCache: true }); + perfTspc({ skipCache: false }); +} + +// endregion + + +/* ****************************************************************************************************************** * + * Entry + * ****************************************************************************************************************** */ + +if (require.main === module) perfAll(); diff --git a/test/src/prepare.ts b/test/src/prepare.ts new file mode 100644 index 0000000..a0746f4 --- /dev/null +++ b/test/src/prepare.ts @@ -0,0 +1,10 @@ +import { cleanTemp } from './project'; + + +export default function() { + try { + cleanTemp(); + } catch (e) { + console.error(e); + } +} diff --git a/test/src/project.ts b/test/src/project.ts new file mode 100644 index 0000000..c3ae71b --- /dev/null +++ b/test/src/project.ts @@ -0,0 +1,134 @@ +import path from 'path'; +import { PackageManager, projectsDir, rootDir } from './config'; +import fs from 'fs'; +import * as os from 'os'; +import shell from 'shelljs'; +import { PartialSome } from './utils/general'; +import { execSync } from 'child_process'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const pkgManagerInstallCmd = { + npm: 'npm install --no-audit --progress=false', + yarn: 'yarn --no-progress --check-cache --no-audit', + pnpm: 'npx pnpm install', + yarn3: 'npx yarn install --skip-builds' +} satisfies Record; + +const pkgManagerInstallerCmd = { + npm: '', + yarn: 'npm install --no-save --legacy-peer-deps yarn', + yarn3: 'npm install --no-save --legacy-peer-deps yarn@berry', + pnpm: 'npm install --no-save --legacy-peer-deps pnpm' +} satisfies Record; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface PrepareOptions { + projectName: string; + + /** @default 'latest' */ + tsVersion: string; + + /** @default 'npm' */ + packageManager: PackageManager; + + dependencies?: Record +} + +export namespace PrepareOptions { + export type Configurable = PartialSome> + + export const getDefaults = () => ({ + packageManager: 'npm', + tsVersion: 'latest' + }) satisfies Partial; +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +function execCmd(cmd: string) { + try { + execSync(cmd, { stdio: [ 'ignore', 'pipe', 'pipe' ] }); + } catch (e) { + throw new Error(`Error during project cmd: ${e.stdout?.toString() + '\n' + e.stderr?.toString()}`); + } +} + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +export function getProjectTempPath(projectName?: string, packageManager?: string, wipe?: boolean) { + const tmpBasePath = process.env.TSP_TMP_DIR ?? os.tmpdir(); + const tmpProjectPath = path.resolve(tmpBasePath, '.tsp-test/project', projectName ?? '', packageManager ?? ''); + if (!fs.existsSync(tmpProjectPath)) fs.mkdirSync(tmpProjectPath, { recursive: true }); + else if (wipe) shell.rm('-rf', path.join(tmpProjectPath, '*')); + + return tmpProjectPath; +} + +export function getProjectPath(projectName: string) { + return path.join(projectsDir, projectName); +} + +export function prepareTestProject(opt: PrepareOptions.Configurable) { + const options: PrepareOptions = { ...PrepareOptions.getDefaults(), ...opt }; + const { projectName, packageManager } = options; + + const projectPath = getProjectPath(projectName); + if (!fs.existsSync(projectPath)) throw new Error(`Project "${projectName}" does not exist`); + + const tmpProjectPath = getProjectTempPath(projectName, packageManager, true); + + /* Copy all files from projectPath to tmpProjectPath */ + shell.cp('-R', path.join(projectPath, '*'), tmpProjectPath); + + shell.cd(tmpProjectPath); + + /* Copy ts-patch to node_modules */ + const tspDir = path.join(tmpProjectPath, '.tsp'); + if (!fs.existsSync(tspDir)) fs.mkdirSync(tspDir, { recursive: true }); + shell.cp('-R', path.join(rootDir, 'dist/*'), tspDir); + + /* Install package manager */ + if (pkgManagerInstallerCmd[packageManager]) + execCmd(pkgManagerInstallerCmd[packageManager]); + + /* Install dependencies */ + const pkgJson = JSON.parse(fs.readFileSync(path.join(tmpProjectPath, 'package.json'), 'utf8')); + pkgJson.dependencies = { + ...pkgJson.dependencies, + ...options.dependencies, + 'typescript': options.tsVersion, + 'ts-patch': 'file:./.tsp' + }; + fs.writeFileSync(path.join(tmpProjectPath, 'package.json'), JSON.stringify(pkgJson, null, 2)); + + execCmd(pkgManagerInstallCmd[packageManager]); + + return { projectPath, tmpProjectPath }; +} + +export function cleanTemp() { + if (!process.env.TSP_TMP_DIR) + fs.rmSync(getProjectTempPath(), { recursive: true, force: true, retryDelay: 200, maxRetries: 5 }); +} + +// endregion diff --git a/test/src/setup.js b/test/src/setup.js deleted file mode 100644 index d1c336c..0000000 --- a/test/src/setup.js +++ /dev/null @@ -1,26 +0,0 @@ -const fs = require('fs-extra'); -const path = require('path'); -const { tmpDir, tsModules } = require('./config'); -const tsp = require('ts-patch'); - - -/* ****************************************************************************************************************** * - * Setup - * ****************************************************************************************************************** */ - -module.exports = () => { - process.stdout.write(`\nPatching ${tsModules.length} TS modules...\n\n`) - if (fs.existsSync(tmpDir)) (fs.rmSync || fs.rmdirSync)(tmpDir, { recursive: true }); - - /* Patch all TS */ - for (const tsm of tsModules) { - try { - const srcDir = tsm.tsDir; - const destDir = path.join(tmpDir, tsm.moduleSpecifier); - fs.copySync(srcDir, destDir, { recursive: true }); - tsp.install({ silent: true, dir: destDir }); - } catch (e) { - console.error(e.toString()); - } - } -} diff --git a/test/src/utils.ts b/test/src/utils.ts deleted file mode 100644 index e55bac9..0000000 --- a/test/src/utils.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as path from 'path'; -import { default as mock } from 'mock-fs'; -import { assetsDir, rootDir, testRootDir, tsModules } from './config'; -import { BACKUP_DIRNAME } from 'ts-patch/src/installer/lib/actions'; -import fs from 'fs'; -import os from 'os'; - -/* ****************************************************************************************************************** * - * Locals - * ****************************************************************************************************************** */ - -const nodeModulesRoot = path.join(testRootDir, 'node_modules'); - -function loadDirs(mockBase: string, dir: string, predicate?: (p: string) => boolean, lazy?: boolean) { - let dirs = fs.readdirSync(dir, { withFileTypes: true }) - .map(entry => [ entry.name, joinPaths(mockBase, entry.name), joinPaths(dir, entry.name) ]) - - if (predicate) dirs = dirs.filter(([ dirName ]) => predicate(dirName)); - - return Object.fromEntries(dirs.map(([ _, mockPath, fullPath ]) => [ mockPath, mock.load(fullPath, { lazy }) ])); -} - -/* ****************************************************************************************************************** */ -// region: Mock Utils -/* ****************************************************************************************************************** */ - -export function mockFs(tsModuleSpecifier: string = 'ts-latest') { - mockFs.initialize(tsModuleSpecifier); - resetFs(); -} - -export function resetFs() { - mock.restore(); - mock({ ...mockFs.cachedMapping, ...mockFs.staticMapping }); -} - -export function restoreFs() { - mock.restore(); -} - -export namespace mockFs { - let currentModuleSpecifier: string; - - export let cachedMapping: any; - export let staticMapping: any; - - export const mockRootDir = os.platform() === 'win32' ? 'C:\\' : '/'; - export const nodeModulesDir = path.join(mockRootDir, 'node_modules'); - export const tspDir = path.join(nodeModulesDir, 'ts-patch'); - export const tsDir = path.join(nodeModulesDir, 'typescript'); - export const tsLibDir = path.join(nodeModulesDir, 'typescript/lib'); - export const tsBackupDir = path.join(nodeModulesDir, 'typescript/' + BACKUP_DIRNAME); - - let _cached: any; - let _static: any; - - export function initialize(tsModuleSpecifier: string) { - const oldModuleSpecifier = currentModuleSpecifier; - currentModuleSpecifier = tsModuleSpecifier; - - Object.defineProperties(mockFs, { - cachedMapping: { - get() { - if (currentModuleSpecifier !== oldModuleSpecifier || !_cached) - _cached = { - ...loadDirs( - mockFs.tspDir, - path.join(nodeModulesRoot, 'ts-patch'), - (dirName) => dirName !== 'test' && dirName[0] !== '.', - false - ), - [mockFs.tsDir]: mock.load(tsModules.find(m => m.moduleSpecifier === tsModuleSpecifier)!.tsDir, { lazy: false }), - }; - - return _cached; - } - }, - staticMapping: { - get() { - _static ??= { - ...loadDirs(mockFs.nodeModulesDir, nodeModulesRoot, (dirName) => dirName !== 'ts-patch'), - [path.join(mockFs.mockRootDir, '/package.json')]: mock.load(path.join(assetsDir, 'test-package.json')), - [path.join(rootDir, 'dist')]: mock.load(path.join(rootDir, 'dist')) - }; - - return _static; - } - } - }); - } -} - -// endregion - - -/* ****************************************************************************************************************** */ -// region: General Utils -/* ****************************************************************************************************************** */ - -export const joinPaths = (...p: string[]) => path.join(...p).replace(/\\+/g, '/'); - -// endregion diff --git a/test/src/utils/general.ts b/test/src/utils/general.ts new file mode 100644 index 0000000..0da978f --- /dev/null +++ b/test/src/utils/general.ts @@ -0,0 +1,8 @@ + +/* ****************************************************************************************************************** */ +// region: Type Utils +/* ****************************************************************************************************************** */ + +export type PartialSome = Omit & Pick, K> + +// endregion diff --git a/test/tests/actions.test.ts b/test/tests/actions.test.ts new file mode 100644 index 0000000..abab743 --- /dev/null +++ b/test/tests/actions.test.ts @@ -0,0 +1,269 @@ +import fs from 'fs'; +import { check } from '../../dist/actions'; +import { TsModule } from '../../dist/module'; +import { defaultInstallLibraries } from '../../dist/config'; +import { getTsPackage, TsPackage } from '../../dist/ts-package'; +import { PackageManager } from '../src/config'; +import { prepareTestProject } from '../src/project'; +import path from 'path'; +import { InstallerOptions } from '../../dist'; +import { LogLevel } from '../../dist/system'; +import { execSync } from 'child_process'; +import ts from 'typescript'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const verboseMode = !!process.env.VERBOSE; + +/* Options to use with install/uninstall */ +const testingPackageManagers = [ + 'npm', + 'yarn', + 'pnpm', + // 'yarn3' +] satisfies PackageManager[]; + +const tspOptions: Partial = { + logLevel: verboseMode ? LogLevel.verbose : LogLevel.system, + silent: !verboseMode +}; + +// endregion + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +function getModulesSources(tsPackage: TsPackage, moduleNames?: string[]) { + moduleNames ??= defaultInstallLibraries; + return new Map(moduleNames.map(name => { + const modulePath = tsPackage.getModulePath(name); + const dtsPath = modulePath.replace(/\.js$/, '.d.ts'); + const js = fs.readFileSync(modulePath, 'utf-8'); + const dts = fs.existsSync(dtsPath) ? fs.readFileSync(dtsPath, 'utf-8') : undefined; + + return [ name, { js, dts } ]; + })); +} + +function updatePackageJson(pkgPath: string, cb: (pkgJson: any) => void) { + let pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + cb(pkgJson); + fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2)); +} + +function resetRequireCache(dir: string) { + dir = path.dirname(require.resolve(dir)); + for (const key in require.cache) { + if (key.startsWith(dir)) { + delete require.cache[key]; + } + } +} + +function runAction(tspDir: string, kind: 'api' | 'cli', cmd: string) { + switch (kind) { + case 'api': + const scriptCode = ` + require('ts-patch').${cmd} + `; + + fs.writeFileSync(path.join(tspDir, 'run-cmd.js'), scriptCode, 'utf-8'); + execSync(`node run-cmd.js`, { cwd: tspDir }); + break; + case 'cli': + const flags = verboseMode ? `--verbose` : '--silent'; + execSync(`ts-patch ${cmd} ${flags}`, { cwd: tspDir }); + } + + resetRequireCache(tspDir); + const { getTsPackage } = require(path.join(tspDir, 'ts-package.js')); + const { getTsModule } = require(path.join(tspDir, 'module')); + const tsPackage = getTsPackage(); + const modules = defaultInstallLibraries.map((m: any) => getTsModule(tsPackage, m)); + + return { modules, tsPackage }; +} + +function runInstall(tspDir: string, kind: 'api' | 'cli') { + let cmd: string; + if (kind === 'api') { + cmd = `install(${JSON.stringify(tspOptions)})`; + } else { + cmd = `install ${verboseMode ? '--verbose' : '--silent'}`; + } + + return runAction(tspDir, kind, cmd); +} + +function runUninstall(tspDir: string, kind: 'api' | 'cli') { + let cmd: string; + if (kind === 'api') { + const tspOptions: Partial = { + logLevel: LogLevel.verbose, + }; + cmd = `uninstall(${JSON.stringify(tspOptions)})`; + } else { + cmd = `uninstall --verbose`; + } + + return runAction(tspDir, kind, cmd); +} + +// endregion + + +/* ******************************************************************************************************************** + * Tests + * ********************************************************************************************************************/ + +describe(`TSP Actions`, () => { + // TODO - Parallelize + describe.each(testingPackageManagers)(`%s`, (packageManager) => { + /* Install */ + describe(`Install`, () => { + let projectPath: string; + let tmpProjectPath: string; + let tspDir: string; + let tsDir: string; + let cachePath: string; + let originalModulesSrc: Map; + beforeAll(() => { + const prepRes = prepareTestProject({ projectName: 'main', packageManager }); + projectPath = prepRes.projectPath; + tmpProjectPath = prepRes.tmpProjectPath; + + const tsPackage = getTsPackage(tsDir); + originalModulesSrc = getModulesSources(tsPackage); + + tspDir = path.resolve(tmpProjectPath, 'node_modules', 'ts-patch'); + tsDir = path.resolve(tmpProjectPath, 'node_modules', 'typescript'); + cachePath = path.resolve(tspDir, '../.cache/ts-patch'); + }); + + describe.each([ [ '', '0.0.0' ], [ ' (overwrite w/ higher version)', '1.1.1' ] ])(`Install %s`, (caption, installedTspVersion) => { + let tsPackage: TsPackage; + let modules: TsModule[]; + beforeAll(() => { + /* Set version */ + updatePackageJson(path.join(tspDir, 'package.json'), (pkgData) => pkgData.version = installedTspVersion); + + tsPackage = getTsPackage(tsDir); + + /* Install */ + const runRes = runInstall(tspDir, 'api'); + + modules = runRes.modules; + }); + + test(`Original modules backed up`, () => { + for (const m of modules) { + const origSrcEntry = originalModulesSrc.get(m.moduleName)!; + if (m.dtsPath) { + const backupSrc = fs.readFileSync(m.backupCachePaths.dts!, 'utf-8'); + expect(backupSrc).toBe(origSrcEntry.dts); + } + + const backupSrc = fs.readFileSync(m.backupCachePaths.js!, 'utf-8'); + expect(backupSrc).toBe(origSrcEntry.js); + } + }); + + test(`All modules patched`, () => { + modules.forEach(m => { + expect(m.isPatched).toBe(true); + expect(m.moduleFile.patchDetail?.tspVersion).toBe(installedTspVersion); + }) + }); + + test(`check() is accurate`, () => { + const checkResult = check(undefined, tspOptions); + const unpatchedModuleNames = tsPackage.moduleNames.filter(m => !defaultInstallLibraries.includes(m)); + unpatchedModuleNames.forEach(m => expect(checkResult[m]).toBeUndefined()); + defaultInstallLibraries.forEach(m => expect(checkResult[m]?.tspVersion).toBe(installedTspVersion)); + }); + + + test(`No semantic errors in typescript.d.ts`, () => { + const dtsFilePath = path.join(tsDir, 'typescript.d.ts'); + + const compilerOptions = Object.assign(ts.getDefaultCompilerOptions(), { + target: 'ES2018', + lib: [ 'es2018' ], + skipDefaultLibCheck: true + }); + + const program = ts.createProgram([ dtsFilePath ], compilerOptions); + const diagnostics = program.getSemanticDiagnostics(); + + // Using toHaveLength causes indefinite hang + expect(diagnostics.length).toBe(0); + }); + }); + }); + + /* Uninstall */ + describe(`Uninstall`, () => { + let projectPath: string; + let tmpProjectPath: string; + let tspDir: string; + let tsDir: string; + let cachePath: string; + let modules: TsModule[]; + let originalModulesSrc: Map; + let tsPackage: TsPackage; + beforeAll(() => { + const prepRes = prepareTestProject({ projectName: 'main', packageManager }); + projectPath = prepRes.projectPath; + tmpProjectPath = prepRes.tmpProjectPath; + + tsPackage = getTsPackage(tsDir); + originalModulesSrc = getModulesSources(tsPackage); + + tspDir = path.resolve(tmpProjectPath, 'node_modules', 'ts-patch'); + tsDir = path.resolve(tmpProjectPath, 'node_modules', 'typescript'); + cachePath = path.resolve(tspDir, '../.cache/ts-patch'); + + /* Install */ + let runRes = runInstall(tspDir, 'api'); + modules = runRes.modules; + modules.forEach(m => { + expect(m.isPatched).toBe(true); + }); + + /* Uninstall */ + runRes = runUninstall(tspDir, 'api'); + modules = runRes.modules; + }); + + test(`All modules unpatched`, () => { + modules.forEach(m => { + expect(m.isPatched).toBe(false); + expect(m.moduleFile.patchDetail).toBeUndefined(); + }); + }); + + test(`All files match originals`, () => { + for (const m of modules) { + const origSrcEntry = originalModulesSrc.get(m.moduleName)!; + if (m.dtsPath) { + const src = fs.readFileSync(m.dtsPath, 'utf-8'); + expect(src).toBe(origSrcEntry.dts); + } + + const src = fs.readFileSync(m.modulePath, 'utf-8'); + expect(src).toBe(origSrcEntry.js); + } + }); + + test(`check() is accurate`, () => { + const checkResult = check(undefined, tspOptions); + tsPackage.moduleNames.forEach(m => expect(checkResult[m]).toBeUndefined()); + }); + }); + }); +}); diff --git a/test/tests/installer/actions.test.ts b/test/tests/installer/actions.test.ts deleted file mode 100644 index 1d4675c..0000000 --- a/test/tests/installer/actions.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import fs from 'fs'; -import { - check, defaultInstallLibraries, install, parseFiles, patch, setOptions, SRC_FILES, uninstall -} from '../../../src/installer/lib/actions'; -import { joinPaths, mockFs, resetFs, restoreFs } from '../../src/utils'; -import ts from 'typescript'; -import { tspPackageJSON } from '../../../src/installer/lib/system'; -// noinspection ES6PreferShortImport -import { getTSPackage } from '../../../src/installer/lib/file-utils'; - - -/* ****************************************************************************************************************** */ -// region: Config -/* ****************************************************************************************************************** */ - -/* Options to use with install/uninstall */ -const TSP_OPTIONS = { silent: true }; - -// endregion - - -/* ****************************************************************************************************************** */ -// region: Helpers -/* ****************************************************************************************************************** */ - -function checkModules( - expectedPatchVersion: string | undefined, - dir: string, - filenames: string[] = SRC_FILES -) -{ - const modules = parseFiles(filenames, dir); - expect(modules.map(m => m.filename)).toEqual(filenames); - for (let { canPatch, patchVersion } of modules) - expect({ canPatch, patchVersion }).toEqual({ canPatch: true, patchVersion: expectedPatchVersion }); -} - -function updatePackageJson(pkgPath: string, cb: (pkgJson: any) => void) { - let pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); - cb(pkgJson); - fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2)); -} - -// endregion - - -/* ******************************************************************************************************************** - * Tests - * ********************************************************************************************************************/ - -describe(`Actions`, () => { - const { tsDir, tspDir, mockRootDir, tsLibDir, tsBackupDir, nodeModulesDir } = mockFs; - beforeAll(() => { - mockFs(); - }); - afterAll(() => { - jest.restoreAllMocks(); - restoreFs(); - }); - - test(`Set Options`, () => { - expect(setOptions(TSP_OPTIONS)).toMatchObject(TSP_OPTIONS) - }); - - describe.each([ [ '', '0.0.0' ], [ ' (overwrite w/ higher version)', '1.1.1' ] ]) - (`Install%s`, (caption, version) => { - let originalVersion: string; - beforeAll(() => { - resetFs(); - - /* Set version */ - updatePackageJson(joinPaths(tspDir, 'package.json'), (pkgData) => pkgData.version = version); - originalVersion = tspPackageJSON.version; - tspPackageJSON.version = version; - - /* Install */ - install(TSP_OPTIONS); - }); - afterAll(() => { - tspPackageJSON.version = originalVersion - }); - - test(`Original modules backed up`, () => { - checkModules(undefined, tsBackupDir, defaultInstallLibraries) - }); - - test(`All modules patched`, () => { - checkModules(version, tsLibDir, defaultInstallLibraries) - }); - - test(`Backs up and patches typescript.d.ts`, () => { - const backupSrc = fs.readFileSync(joinPaths(tsBackupDir, 'typescript.d.ts'), 'utf-8'); - expect(backupSrc).toMatch(/declare\snamespace\sts\s{/); - expect(backupSrc).not.toMatch(/const\stspVersion:/); - - const patchSrc = fs.readFileSync(joinPaths(tsLibDir, 'typescript.d.ts'), 'utf-8'); - expect(patchSrc).toMatch(/declare\snamespace\sts\s{/); - expect(patchSrc).toMatch(/const\stspVersion:/); - }); - - test(`Config file is correct`, () => { - const file = joinPaths(tsDir, 'ts-patch.json'); - expect(fs.existsSync(file)).toBe(true); - - const config = getTSPackage(tsDir).config; - - expect(config).toMatchObject({ version: version }); - - expect(Object - .entries(config.modules) - .filter(([ filename, timestamp ]) => - SRC_FILES.includes(filename) && // Filename must be valid - !isNaN(parseFloat(timestamp)) // Timestamp must be valid - ) - .length - ).toBe(defaultInstallLibraries.length); - }); - - test(`check() is accurate`, () => { - const modules = check(SRC_FILES); - expect(modules.map(({ filename }) => filename)).toEqual(SRC_FILES); - expect(modules.unPatchable.length).toEqual(0); - expect(modules.canUpdateOrPatch.map(m => m.filename)).toEqual(SRC_FILES.filter(s => !defaultInstallLibraries.includes(s))); - expect(modules.patched.map(m => m.filename)).toEqual(defaultInstallLibraries); - expect(modules.patchable.map(m => m.filename)).toEqual(SRC_FILES); - }); - - // Leave this as the final test, as it resets the virtual FS - test(`No semantic errors in typescript.d.ts`, () => { - const tsDtsFileSrc = fs.readFileSync(joinPaths(tsBackupDir, 'typescript.d.ts'), 'utf-8'); - restoreFs(); - - const compilerOptions = Object.assign(ts.getDefaultCompilerOptions(), { - target: 'ES5', - lib: [ "es2015" ], - skipDefaultLibCheck: true - }); - const host = ts.createCompilerHost(compilerOptions, false); - const originalReadFile = host.readFile; - host.readFile = fileName => (fileName === 'typescript.d.ts') ? tsDtsFileSrc : originalReadFile(fileName); - - const program = ts.createProgram([ 'typescript.d.ts' ], compilerOptions, host); - const diagnostics = program.getSemanticDiagnostics(); - - // Using toHaveLength causes indefinite hang - expect(diagnostics.length).toBe(0); - }); - }); - - describe(`Uninstall`, () => { - beforeAll(() => { - resetFs(); - install(TSP_OPTIONS); - uninstall(TSP_OPTIONS); - }); - - test(`Removes backup directory`, () => { - expect(fs.existsSync(tsBackupDir)).toBe(false) - }); - - test(`Restores original modules`, () => checkModules(undefined, tsLibDir, defaultInstallLibraries)); - - test(`Restores typescript.d.ts`, () => { - const src = fs.readFileSync(joinPaths(tsLibDir, 'typescript.d.ts'), 'utf-8'); - expect(src).toMatch(/declare\snamespace\sts\s{/); - expect(src).not.toMatch(/const\stspVersion:/); - }); - - test(`check() is accurate`, () => { - const modules = check(SRC_FILES); - expect(modules.unPatchable.length).toEqual(0); - expect(modules.canUpdateOrPatch.length).toEqual(SRC_FILES.length); - expect(modules.patched.length).toEqual(0); - expect(modules.patchable.length).toEqual(SRC_FILES.length); - }); - }); - - describe(`Patch`, () => { - beforeEach(() => { - resetFs(); - }); - - test(`Patches single file`, () => { - patch(SRC_FILES[0], TSP_OPTIONS); - checkModules(tspPackageJSON.version, tsLibDir, [ SRC_FILES[0] ]); - }); - - test(`Patches array of files`, () => { - patch(SRC_FILES, TSP_OPTIONS); - checkModules(tspPackageJSON.version, tsLibDir); - }); - - test(`Patches glob`, () => { - const srcFileNames = SRC_FILES.map(f => f.split('.')[0]); - const globStr = joinPaths(tsLibDir, `{${srcFileNames.join(',')}}.js`); - patch(globStr, TSP_OPTIONS); - checkModules(tspPackageJSON.version, tsLibDir); - }); - - test(`Throws with TS version < 4.0`, () => { - const pkgPath = joinPaths(tsDir, 'package.json'); - fs.writeFileSync(pkgPath, - fs.readFileSync(pkgPath, 'utf-8') - .replace(/"version": ".+?"/, `"version": "3.9.9"`) - ); - let err: Error | undefined; - try { patch(SRC_FILES, TSP_OPTIONS); } - catch (e) { err = e; } - expect(err && err.name).toBe('WrongTSVersion'); - }); - }); -}); diff --git a/test/tests/installer/cli.test.ts b/test/tests/installer/cli.test.ts deleted file mode 100644 index 4e786d3..0000000 --- a/test/tests/installer/cli.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import stripAnsi from 'strip-ansi'; -import { run as runFn, cliCommands, cliOptions } from '../../../src/installer/bin/cli'; -import { getGlobalTSDir } from '../../../src/installer/lib/file-utils'; -import * as actions from '../../../src/installer/lib/actions'; - -/* ****************************************************************************************************************** */ -// region: Helpers & Config -/* ****************************************************************************************************************** */ - -const opts = { color: false }; - -const run = (...args: any[]) => { - setOptions(opts); - return runFn(args.join(' ')); -}; - -const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - -// endregion - - -/* ****************************************************************************************************************** */ -// region: Module Mocks -/* ****************************************************************************************************************** */ - -jest.mock('ts-patch/src/installer/lib/actions'); -const { setOptions } = jest.requireActual('ts-patch/src/installer/lib/actions'); - -// endregion - - - -/* ******************************************************************************************************************** - * Tests - * ********************************************************************************************************************/ - -describe(`CLI`, () => { - let logSpy: jest.SpyInstance; - beforeAll(() => { - logSpy = jest.spyOn(console, 'log').mockImplementation(); - }); - - afterAll(() => { - logSpy.mockRestore(); - jest.unmock('ts-patch/src/installer/lib/actions') - }); - - afterEach(() => jest.resetAllMocks()); - - describe(`Action commands run`, () => { - test(`install`, () => { - run('install'); - expect(actions.install).toBeCalled(); - }); - - test(`uninstall`, () => { - run('uninstall'); - expect(actions.uninstall).toBeCalled() - }); - - test(`patch`, () => { - run('patch'); - expect(actions.patch).toBeCalled() - }); - - test(`unpatch`, () => { - run('unpatch'); - expect(actions.unpatch).toBeCalled() - }); - - test(`check`, () => { - run('check'); - expect(actions.check).toBeCalled() - }); - }); - - describe(`Log commands output`, () => { - test(`version`, () => { - run('version'); - expect(/ts-patch:/g.test(logSpy.mock.calls.pop().join(' '))).toBe(true) - }); - - test(`Invalid command throws`, () => { - run('notACommand_'); - expect(/Invalid command/g.test(logSpy.mock.calls.pop().join(' '))).toBe(true) - }); - - describe(`help`, () => { - let output: string; - beforeAll(() => { - logSpy.mockReset(); - run('help'); - output = logSpy.mock.calls.pop().join(' '); - }); - - test( - `Header appears`, - () => expect(/^\s*ts-patch \[command]/m.test(output)).toBe(true) - ); - - test(`All commands appear`, () => { - for (let [ cmd, { short, caption, paramCaption } ] of (Object.entries(cliCommands) as [ string, any ])) { - caption = stripAnsi(caption); - paramCaption = stripAnsi(paramCaption); - - const regexStr = - String.raw`^\s*` + - String.raw`${escape(cmd)},?\s+?` + - (short ? String.raw`${escape(short)}\s+?` : '') + - (paramCaption ? String.raw`${escape(paramCaption)}\s+?` : '') + - String.raw`\.+?\s+?` + - escape(caption) + - `$`; - - expect({ [cmd]: new RegExp(regexStr, 'm').test(output) }).toEqual({ [cmd]: true }); - } - }); - - test(`All args appear`, () => { - for (let [ arg, { short, caption, paramCaption, inverse } ] of (Object.entries(cliOptions) as [ string, any ])) { - caption = stripAnsi(caption); - paramCaption = stripAnsi(paramCaption); - if (inverse) arg = `no-${arg}`; - - const regexStr = - String.raw`^\s*` + - (short ? String.raw`-${escape(short)},\s+?` : '') + - String.raw`--${escape(arg)}\s+?` + - (paramCaption ? String.raw`${escape(paramCaption)}\s+?` : '') + - String.raw`\.+?\s+?` + - escape(caption) + - `$`; - - expect({ [arg]: new RegExp(regexStr, 'm').test(output) }).toEqual({ [arg]: true }); - } - }); - }); - }); - - describe(`Parses all options`, () => { - test(`--silent, --verbose`, () => { - expect(run('v')!.options).toMatchObject({ silent: false, verbose: false }); - expect(run('v', '-s -v')!.options).toMatchObject({ silent: true, verbose: true }); - expect(run('v', '--silent --verbose')!.options).toMatchObject({ silent: true, verbose: true }); - }); - - test(`--global`, () => { - const globalTSDir = (() => { - try { return getGlobalTSDir(); } - catch (e) { return undefined } - })(); - - if (globalTSDir) { - expect(run('v', '-g')!.options).toMatchObject({ dir: globalTSDir }); - expect(run('v', '--global')!.options).toMatchObject({ dir: globalTSDir }); - } else { - run('v', '-g'); - expect(logSpy.mock.calls.pop().join(' ')).toMatch(/Could not find global TypeScript installation!/); - logSpy.mockReset(); - run('v', '--global'); - expect(logSpy.mock.calls.pop().join(' ')).toMatch(/Could not find global TypeScript installation!/); - } - - // Throw with both global and dir - run('v', '-g -d /file/path'); - expect(logSpy.mock.calls.pop().join(' ')).toMatch('Cannot specify both'); - }); - - test(`--dir`, () => { - const warnSpy = jest.spyOn(console, 'warn'); - try { - expect(run('v', `-d /file/path`)!.options).toMatchObject({ dir: '/file/path' }); - expect(run('v', '--dir /file/path')!.options).toMatchObject({ dir: '/file/path' }); - - expect(warnSpy).not.toHaveBeenCalled(); - expect(run('v', '--basedir /file/path')!.options).toMatchObject({ dir: '/file/path' }); - expect(warnSpy.mock.calls.slice(-1)[0][0]).toMatch('Use --dir instead'); - } finally { - warnSpy.mockRestore(); - } - }); - - test(`Color`, () => { - expect(run('v', '--color')!.options).toMatchObject({ color: true }); - expect(run('v', '--no-color')!.options).toMatchObject({ color: false }); - }); - }); -}); diff --git a/test/tests/installer/logger.test.ts b/test/tests/installer/logger.test.ts deleted file mode 100644 index 573a056..0000000 --- a/test/tests/installer/logger.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Log, TSPOptions } from '../../../src/installer/lib/system'; -import { mockConsoleLog, mockProcessStdout, mockProcessStderr } from 'jest-mock-process'; -// noinspection ES6PreferShortImport -import { setOptions } from '../../../src/installer/lib/actions'; - - -/* ****************************************************************************************************************** */ -// region: Helpers -/* ****************************************************************************************************************** */ - -const hasColor = /\x1b\[[0-9;]*m/; - -function log(opt?: Partial, msg: string | [ string, string ] = [ '=', 'test' ]) { - const { instanceIsCLI } = setOptions(opt); - const logSpy = mockConsoleLog().mockImplementation(); - const stdOutSpy = mockProcessStdout().mockImplementation(); - const stdErrSpy = mockProcessStderr().mockImplementation(); - - const isError = Array.isArray(msg) && (msg[0] === '!'); - - try { - Log(msg); - - let res:string; - if (instanceIsCLI) { - if (isError) { - expect(stdErrSpy).toBeCalled(); - expect(stdOutSpy).not.toBeCalled(); - res = stdErrSpy.mock.calls.pop()![0]; - } else { - expect(stdErrSpy).not.toBeCalled(); - expect(stdOutSpy).toBeCalled(); - res = stdOutSpy.mock.calls.pop()![0]; - } - } - else { - expect(logSpy).toBeCalled(); - res = logSpy.mock.calls.pop()![0]; - } - expect(res).toMatch(Array.isArray(msg) ? msg[1] : msg); - - return res; - } - finally { - logSpy.mockRestore(); - stdOutSpy.mockRestore(); - stdErrSpy.mockRestore(); - } -} - -// endregion - - -/* ******************************************************************************************************************** - * Tests - * ********************************************************************************************************************/ - -describe(`Logger`, () => { - beforeAll(() => setOptions({ instanceIsCLI: false, logLevel: Log.normal })); - - test(`Uses colour if color=true`, () => expect(log({ color: true })).toMatch(hasColor)); - test(`Strips colour if color=false`, () => expect(log({ color: false })).not.toMatch(hasColor)); - - describe(`CLI Mode`, () => { - test(`Log goes to stdout`, () => { log({ instanceIsCLI: true }, [ '=', 'test' ]) }); - test(`Error goes to stderr`, () => { log({ instanceIsCLI: true }, [ '!', 'test' ]) }); - }); -}); diff --git a/test/tests/patch/plugin-creator.test.ts b/test/tests/patch/plugin-creator.test.ts deleted file mode 100644 index fae6348..0000000 --- a/test/tests/patch/plugin-creator.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { PluginConfig, TransformerList } from '../../../src/shared/plugin-types'; -import { advancedTransformer } from '../../assets/transformers/transform-advanced'; -import { simpleTransformer } from '../../assets/transformers/transform-simple'; -import { progTransformer1, progTransformer2 } from '../../assets/transformers/program-transformer'; -import type TS from 'typescript'; -import path from 'path'; -import { assetsDir, tmpDir } from '../../src/config'; - -/* ******************************************************************************************************************** - * Helpers - * ********************************************************************************************************************/ - -function createTransformers(PluginCreator: any, config: PluginConfig[]): TransformerList -{ - const pluginCreator = new PluginCreator(config, __dirname); - const host = { program: {} as TS.Program }; - return pluginCreator.createTransformers(host); -} - -const getAsset = (p: string) => path.join(assetsDir, p); - -/* ******************************************************************************************************************** - * Tests - * ********************************************************************************************************************/ - -describe(`PluginCreator class`, () => { - const ts = require(path.join(tmpDir, 'ts-latest')); - const { PluginCreator } = ts; - - test('PluginCreator initializes', () => { - const pluginCreator = new PluginCreator([]); - expect(pluginCreator).toBeInstanceOf(PluginCreator); - }); - - test('Throws with bad config', () => { - const config = [ { someGarbage: 123 } ] as any; - expect(() => new PluginCreator(config)).toThrow(); - }); - - test('Initializes before transformer ', () => { - const config: PluginConfig[] = [ { transform: getAsset('transformers/transform-simple.ts') } ]; - expect(createTransformers(PluginCreator, config)).toEqual({ - after: [], - afterDeclarations: [], - before: [ simpleTransformer ], - }); - }); - - test('Initializes after transformer', () => { - const config: PluginConfig[] = [ { transform: getAsset('transformers/transform-simple.ts'), after: true } ]; - expect(createTransformers(PluginCreator, config)).toEqual({ - after: [ simpleTransformer ], - afterDeclarations: [], - before: [], - }); - }); - - test('Initialize advanced after transformer', () => { - const config: PluginConfig[] = [ { transform: getAsset('transformers/transform-advanced.ts') } ]; - expect(createTransformers(PluginCreator, config)).toEqual({ - after: [ advancedTransformer ], - afterDeclarations: [], - before: [], - }); - }); - - test('Works with custom config', () => { - const config: PluginConfig[] = [ { transform: getAsset('transformers/transform-advanced.ts'), some: 1, bla: 2 } as any ]; - expect(createTransformers(PluginCreator, config)).toEqual({ - after: [ advancedTransformer ], - afterDeclarations: [], - before: [], - }); - }); - - test('Initializes Program transformers', () => { - const config: PluginConfig[] = [ - { transform: getAsset('transformers/program-transformer.ts'), transformProgram: true, import: 'progTransformer1' }, - { transform: getAsset('transformers/program-transformer.ts'), beforeEmit: true, import: 'progTransformer2' } - ]; - - const programTransformers = new Map(new PluginCreator(config, __dirname).getProgramTransformers()); - expect(programTransformers.get(progTransformer1)).toEqual(config[0]); - expect(programTransformers.get(progTransformer2)).toEqual(config[1]); - }); -}); diff --git a/test/tests/patch/tsc.test.ts b/test/tests/patch/tsc.test.ts deleted file mode 100644 index baff30a..0000000 --- a/test/tests/patch/tsc.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import path from 'path'; -import vm from 'vm'; -import module from 'module'; -import { mockProcessExit, mockProcessStdout, mockProcessStderr } from 'jest-mock-process'; -import { normalizeSlashes } from 'typescript'; -import { assetsDir, tmpDir, tsModules } from '../../src/config'; -import fs from 'fs'; -import child_process from 'child_process'; - - -/* ****************************************************************************************************************** */ -// region: Vars -/* ****************************************************************************************************************** */ - -const expectedOut = - /^[\s\r\n]*function type\(\) {[\s\r\n]*return '';*[\s\r\n]*}[\s\r\n]*var x = "{ abc: 1; }";*[\s\r\n]*console.log\(x\);*$/m; - -const transformedFile = normalizeSlashes(path.resolve(assetsDir, 'src-files/tsnode-code.js')); - -// endregion - - -/* ****************************************************************************************************************** */ -// region: Helpers -/* ****************************************************************************************************************** */ - -function execTsc(tscPath: string, tscScript: vm.Script, cmd: string): - { message: string | undefined, err: string | undefined, files: Record, code: string } -{ - const args = cmd.split(' '); - - const outFiles = >{}; - - /* Mocks */ - const mockStdOut = mockProcessStdout(); - const mockStdErr = mockProcessStderr(); - const mockExit = mockProcessExit(); - - try { - const writeFile = jest.fn( - (fileName: any, data: any) => outFiles[String(fileName)] = String(data) - ); - - /* Execute TSC */ - let thrownErr: string[] = []; - try { - tscScript.runInThisContext()( - exports, - require, - module, - /* __filename */ tscPath, - /* __dirname */ path.dirname(tscPath), - args, - process, - writeFile, - console - ); - } catch (e) { - thrownErr.push(String(e)); - } - - return ({ - message: mockStdOut.mock.calls.join('\n'), - err: [ ...mockStdErr.mock.calls, ...thrownErr ].join('\n'), - files: outFiles, - code: String(mockExit.mock.calls.pop()) - }); - } finally { - mockStdOut.mockRestore(); - mockStdErr.mockRestore(); - mockExit.mockRestore(); - } -} - -// endregion - - -/* ******************************************************************************************************************** - * Tests - * ********************************************************************************************************************/ - -describe(`TSC`, () => { - describe.each(tsModules)(`TS $label`, ({ moduleSpecifier }) => { - let tscPath: string; - let tscScript: vm.Script; - beforeAll(() => { - tscPath = path.join(tmpDir, moduleSpecifier, 'lib/tsc.js'); - const tscCode = fs.readFileSync(tscPath, 'utf8'); - const code = `(function (exports, require, module, __filename, __dirname, tscArgs, process, mockWriteFile, console) { \n` + - tscCode.replace( - /(^\s*?ts\.executeCommandLine\(ts\.sys)/m, - `\nObject.assign(ts.sys, { args: tscArgs, writeFile: mockWriteFile });\n$1` - ) + - `})`; - - tscScript = new vm.Script(code, { filename: tscPath, displayErrors: false }); - }); - - test('tsc transforms code & outputs standard diagnostic', () => { - const { - code, - message, - files, - err - } = execTsc(tscPath, tscScript, `--noEmit false -p ${path.join(assetsDir, 'src-files')}`); - - expect(err).toBe(''); - expect(code).toBe('2'); - expect(message).toMatch(/TS2339/); - expect(files[transformedFile]).toMatch(expectedOut); - }); - - describe(`Diagnostics`, () => { - let code: string; - let errors: string; - let message: string; - beforeAll(() => { - const res = execTsc( - tscPath, - tscScript, - `--noEmit false -p ${path.join(assetsDir, 'src-files/tsconfig.alter-diags.json')}` - ); - code = res.code; - message = res.files[transformedFile]; - errors = res.message!; - }); - - test('Has proper exit code', () => { - expect(code).toBe('2') - }) - - test(`Diagnostics array passed`, () => { - expect(message).toMatch(/DIAG_PASSED=true/) - }); - - test(`Found original error code`, () => { - expect(message).toMatch(/FOUND_ERROR=true/) - }); - - test(`'library' is 'tsc'`, () => { - expect(message).toMatch(/LIBRARY=tsc/) - }); - - test(`removeDiagnostic works`, () => { - expect(errors).not.toMatch(/TS2339/); - }); - - test(`addDiagnostic works`, () => { - expect(errors).toMatch(/TS1337/) - }); - }); - - describe(`Path Mapping`, () => { - const mappingProjectPath = path.join(assetsDir, 'src-files/mapping-project'); - test(`Warns without tsconfig-paths`, () => { - const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); - jest.doMock( - "tsconfig-paths", - () => { - require("sdf0s39rf3333d@fake-module"); - }, - { virtual: true } - ); - try { - const res = execTsc(tscPath, tscScript, `--noEmit false -p ${mappingProjectPath}`); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls.slice(-1)[0][0]).toMatch(`try adding 'tsconfig-paths'`) - } finally { - jest.dontMock('tsconfig-paths'); - warnSpy.mockRestore() - } - }); - - test(`Does not warn without tsconfig-paths`, () => { - const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); - try { - const res = execTsc(tscPath, tscScript, `--noEmit false -p ${mappingProjectPath}`); - expect(warnSpy).not.toHaveBeenCalled(); - } finally { - warnSpy.mockRestore(); - } - }); - - test(`Loads project file & path mapping works`, () => { - const cmd = `node ${tscPath} --noEmit false -p ${mappingProjectPath}`; - const res = child_process.spawnSync(cmd, { stdio: 'pipe', shell: true }); - expect(res.stdout.toString()).toMatch(/Path-Mapping Success!/); - }); - - test(`Mapping fails without project specified`, () => { - const cmd = `node ${tscPath} --noEmit false -p ${path.join(mappingProjectPath, 'tsconfig.noproject.json')}`; - const res = child_process.spawnSync(cmd, { stdio: 'pipe', shell: true }); - expect(res.stderr.toString()).toMatch(/Cannot find module '#a'/); - }); - }); - }); -}); diff --git a/test/tests/patch/typescript.test.ts b/test/tests/patch/typescript.test.ts deleted file mode 100644 index 67886d6..0000000 --- a/test/tests/patch/typescript.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import type TS from 'typescript'; -import { ScriptTarget } from 'typescript'; -import * as programTransformers from '../../assets/transformers/program-transformer'; -import { newFiles } from '../../assets/transformers/program-transformer'; -import SpyInstance = jest.SpyInstance; -import { assetsDir, tmpDir, tsModules } from '../../src/config'; - -/* ******************************************************************************************************************** - * Locals - * ********************************************************************************************************************/ - -const safelyCodePath = path.join(assetsDir, 'src-files/safely-code.ts'); -const safelyCode = fs.readFileSync(safelyCodePath).toString(); -const safelyExpected = - /^var a = { b: 1 };*[\s\r\n]*function abc\(\) {[\s\r\n]*var c = a && a.b;*[\s\r\n]*}[\s\r\n]*console.log\(abc.toString\(\)\);*$/m; - -const basicCode = 'var a = 1'; -const basicExpected = /^[\s\r\n]*var a = 1;*[\s\r\n]*$/m; - -const defaultCompilerOptions = { - lib: undefined, - types: undefined, - noEmit: undefined, - noEmitOnError: undefined, - paths: undefined, - rootDirs: undefined, - composite: undefined, - declarationDir: undefined, - out: undefined, - outFile: undefined, - noResolve: true, - noLib: true -} - -const getAsset = (p: string) => path.join(assetsDir, p); - - -/* ******************************************************************************************************************** - * Tests - * ********************************************************************************************************************/ - -describe(`Typescript`, () => { - describe.each(tsModules)(`TS : $label`, ({ moduleSpecifier }) => { - const ts: typeof TS = require(path.join(tmpDir, moduleSpecifier, 'lib/typescript.js')); - const compilerOptions = { ...defaultCompilerOptions, ...ts.getDefaultCompilerOptions() }; - - test(`Throws if ts-node not present`, () => { - jest.doMock( - 'ts-node', - () => ({ register: () => { require('sdf0s39rf3333d@fake-module') } }), - { virtual: true } - ); - - try { - expect(() => ts.transpileModule(safelyCode, { - compilerOptions: { - plugins: [ { - customTransformers: { before: [ getAsset('/transformers/safely.ts') ] }, - } ] as any, - }, - })).toThrow(`Cannot use a typescript-based transformer without ts-node installed.`) - } finally { - jest.dontMock('ts-node'); - } - }); - - test('Applies transformer from legacy config', () => { - const res = ts.transpileModule(safelyCode, { - compilerOptions: { - plugins: [ { - customTransformers: { before: [ getAsset('transformers/safely.ts') ] }, - } ] as any, - }, - }); - - expect(res.outputText).toMatch(safelyExpected); - }); - - test('Applies transformer from default config', () => { - const res = ts.transpileModule(safelyCode, { - compilerOptions: { - plugins: [ { - transform: getAsset('transformers/safely.ts'), - } ] as any, - }, - }); - - expect(res.outputText).toMatch(safelyExpected); - }); - - // see: https://github.com/nonara/ts-patch/issues/59 - describe(`Relative transformer resolution`, () => { - it('Without project: Resolves from cwd', () => { - const res = ts.transpileModule(safelyCode, { - compilerOptions: { - plugins: [ { - transform: './test/assets/transformers/safely.ts', - } ] as any, - }, - }); - - expect(res.outputText).toMatch(safelyExpected); - }); - - test('With project: Resolves from project root', () => { - const compilerOptions = { - skipLibCheck: true, - skipDefaultLibCheck: true, - plugins: [ { - transform: '../transformers/safely.ts', - } ] as any - }; - const program = ts.createProgram([ safelyCodePath ], compilerOptions); - let outputText: string; - program.emit(void 0, (_, src) => outputText = src); - expect(outputText!).toMatch(safelyExpected); - }); - - }); - - test('Merges transformers', () => { - const customTransformer = jest.fn((sf: any) => sf); - - const res = ts.transpileModule(safelyCode, { - compilerOptions: { - plugins: [ { - transform: getAsset('transformers/safely.ts'), - } ] as any, - }, - transformers: { before: [ () => customTransformer ] }, - }); - - expect(res.outputText).toMatch(safelyExpected); - expect(customTransformer).toBeCalledTimes(1); - }); - - describe(`TransformerExtras`, () => { - let message = ''; - let diagnostics: TS.Diagnostic[]; - beforeAll(() => { - const rootDir = path.join(assetsDir, 'src-files'); - const pcl = ts.getParsedCommandLineOfConfigFile( - path.join(rootDir, 'tsconfig.alter-diags.json'), - { noEmit: false }, - ts.sys as any, - )!; - const cwdMock = jest.spyOn(process, 'cwd').mockImplementation(() => rootDir); - const program = ts.createProgram(pcl.fileNames, pcl.options); - - diagnostics = program.emit( - ts.createSourceFile('a', '', ScriptTarget.ES5), - /* writeFile */ (fileName: string, data: any) => message = String(data) - ).diagnostics as TS.Diagnostic[]; - - cwdMock.mockRestore(); - }); - - test(`Diagnostics array passed`, () => { - expect(message).toMatch(/DIAG_PASSED=true/) - }); - - test(`Found original error code`, () => { - expect(message).toMatch(/FOUND_ERROR=true/) - }); - - test(`'library' is 'typescript'`, () => { - expect(message).toMatch(/LIBRARY=typescript/) - }); - - test(`removeDiagnostic works`, () => { - expect(diagnostics.find(({ code }) => code === 2339)).toBeFalsy() - }); - - test(`addDiagnostic works`, () => { - expect(diagnostics.find(({ code }) => code === 1337)).toBeTruthy() - }); - }); - - test('Runs 3rd party transformers', () => { - const res = ts.transpileModule(basicCode, { - compilerOptions: { - plugins: [ - { transform: 'ts-transform-img/dist/transform', type: 'config' }, - { transform: 'ts-transform-react-intl/dist/transform', type: 'config', import: 'transform' }, - ] as any, - }, - }); - - expect(res.outputText).toMatch(basicExpected); - }); - - test('Skips ts plugin without errors', () => { - const res = ts.transpileModule(basicCode, { - compilerOptions: { - plugins: [ { name: 'foobar' } ], - }, - }); - expect(res.outputText).toMatch(basicExpected) - }); - - describe('Program transformer', () => { - const safelyFile = getAsset('src-files/safely-code.ts'); - const pluginsNormal: any[] = [ - { - transform: getAsset('transformers/program-transformer.ts'), - transformProgram: true, - import: 'progTransformer1' - }, - { - transform: getAsset('transformers/program-transformer.ts'), - transformProgram: true, - import: 'progTransformer2' - } - ]; - const pluginsRecursive: any[] = [ - { - transform: getAsset('transformers/program-transformer.ts'), - transformProgram: true, - import: 'recursiveTransformer1' - }, - { - transform: getAsset('transformers/program-transformer.ts'), - transformProgram: true, - import: 'recursiveTransformer2' - } - ]; - let spies: Map; - - beforeAll(() => { - const fns = [ 'progTransformer1', 'progTransformer2', 'recursiveTransformer1', 'recursiveTransformer2' ] as const; - spies = new Map( - fns.map(f => - [ f, jest.spyOn(programTransformers, f) ] - ) - ); - }); - - afterEach(() => spies.forEach(s => s.mockClear())); - afterAll(() => spies.forEach(s => s.mockRestore())); - - test(`Transforms program once`, () => { - const options = { ...compilerOptions, plugins: pluginsNormal.slice(0, 1) }; - const program = ts.createProgram([ safelyFile ], options); - - expect(spies.get('progTransformer1')).toBeCalledTimes(1); - expect(spies.get('progTransformer2')).not.toBeCalled(); - expect(!!program.getSourceFile(newFiles[0])).toBe(true); - expect(!!program.getSourceFile(newFiles[1])).toBe(false); - }); - - test(`Transforms program twice`, () => { - const options = { ...compilerOptions, plugins: pluginsNormal }; - const program = ts.createProgram([ safelyFile ], options); - - expect(spies.get('progTransformer1')).toBeCalledTimes(1); - expect(spies.get('progTransformer2')).toBeCalledTimes(1); - expect(!!program.getSourceFile(newFiles[0])).toBe(false); - expect(!!program.getSourceFile(newFiles[1])).toBe(true); - }); - - test(`Prevents createProgram recursion`, () => { - const options = { ...compilerOptions, plugins: pluginsRecursive } - ts.createProgram([ safelyFile ], options); - - expect(spies.get('recursiveTransformer1')).toBeCalledTimes(1); - expect(spies.get('recursiveTransformer2')).toBeCalledTimes(1); - expect((programTransformers.progTsInstance)?.originalCreateProgram).toBeTruthy(); - }); - }); - }); -}); diff --git a/test/tests/path-mapping.test.ts b/test/tests/path-mapping.test.ts new file mode 100644 index 0000000..54641f0 --- /dev/null +++ b/test/tests/path-mapping.test.ts @@ -0,0 +1,47 @@ +import { prepareTestProject } from '../src/project'; +import { execSync } from 'child_process'; +import path from 'path'; + + +/* ****************************************************************************************************************** * + * Tests + * ****************************************************************************************************************** */ + +describe(`Path Mapping`, () => { + let projectPath: string; + let output: string[]; + + beforeAll(() => { + const prepRes = prepareTestProject({ projectName: 'path-mapping', packageManager: 'yarn' }); + projectPath = prepRes.tmpProjectPath; + + let commandOutput: string; + try { + commandOutput = execSync('tspc', { + cwd: projectPath, + env: { + ...process.env, + PATH: `${projectPath}/node_modules/.bin${path.delimiter}${process.env.PATH}` + } + }).toString(); + } catch (e) { + const err = new Error(e.stdout.toString() + '\n' + e.stderr.toString()); + console.error(err); + throw e; + } + + output = commandOutput.trim().split('\n'); + }); + + test(`Resolves sub-paths`, () => { + expect(output[0]).toEqual('sub-path:true'); + }); + + test(`Resolves direct paths`, () => { + expect(output[1]).toEqual('path:true'); + }); + + test(`Cannot resolve unmapped paths`, () => { + expect(output[2]).toEqual('non-mapped:false'); + }); +}); diff --git a/test/tests/transformer.test.ts b/test/tests/transformer.test.ts new file mode 100644 index 0000000..f20e7bf --- /dev/null +++ b/test/tests/transformer.test.ts @@ -0,0 +1,40 @@ +import { prepareTestProject } from '../src/project'; +import { execSync } from 'child_process'; + + +/* ****************************************************************************************************************** */ +// region: Config +/* ****************************************************************************************************************** */ + +const transformerKinds = [ + 'mts', + 'ts', + 'cts', + 'mjs', + 'cjs' +]; + +// endregion + + +/* ****************************************************************************************************************** * + * Tests + * ****************************************************************************************************************** */ + +describe(`Transformer`, () => { + let projectPath: string; + let loaderResolve: (value?: unknown) => void; + let loaderPromise = new Promise(resolve => loaderResolve = resolve); + beforeAll(() => { + const prepRes = prepareTestProject({ projectName: 'transform', packageManager: 'yarn' }); + projectPath = prepRes.tmpProjectPath; + loaderResolve(); + }); + + test.concurrent.each(transformerKinds)(`%s transformer works`, async (transformerKind: string) => { + await loaderPromise; + + const res = execSync(`node run-transform.js ${transformerKind}`, { cwd: projectPath }); + expect(res.toString('utf8')).toMatch(new RegExp(`^var a = "after-${transformerKind}";?$`, 'm')); + }); +}); diff --git a/test/tests/webpack.test.ts b/test/tests/webpack.test.ts new file mode 100644 index 0000000..453d0c6 --- /dev/null +++ b/test/tests/webpack.test.ts @@ -0,0 +1,63 @@ +import { execSync, ExecSyncOptions } from 'child_process'; +import { prepareTestProject } from '../src/project'; + + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +function execAndGetErr(projectPath: string, projectFile: string = '', hideModules?: string) { + const extraOpts: ExecSyncOptions = { + ...(hideModules ? { env: { ...process.env, HIDE_MODULES: hideModules } } : {}) + }; + + const cmd = `ts-node ${hideModules ? '-r ./hide-module.js' : ''} -C ts-patch/compiler${projectFile ? ` -P ${projectFile}` : ''}` + try { + execSync( + cmd, + { + cwd: projectPath, + stdio: [ 'ignore', 'pipe', 'pipe' ], + ...extraOpts + }); + } catch (e) { + return e.stderr.toString(); + } + + throw new Error('Expected error to be thrown, but none was'); +} + +// endregion + + +/* ****************************************************************************************************************** * + * Tests + * ****************************************************************************************************************** */ + +describe('Webpack', () => { + let projectPath: string; + beforeAll(() => { + const prepRes = prepareTestProject({ projectName: 'webpack', packageManager: 'yarn' }); + projectPath = prepRes.tmpProjectPath; + }); + + test(`Compiler with CJS transformer works`, () => { + const err = execAndGetErr(projectPath); + expect(err).toContain('Error: ts-patch worked (cjs)'); + }); + + test(`Compiler with ESM TS transformer works`, () => { + const err = execAndGetErr(projectPath, './tsconfig.esmts.json'); + expect(err).toContain('Error: ts-patch worked (esmts)'); + }); + + test(`Compiler with ESM JS transformer works`, () => { + const err = execAndGetErr(projectPath, './tsconfig.esm.json'); + expect(err).toContain('Error: ts-patch worked (esm)'); + }); + + test(`Compiler with ESM transformer throws if no ESM package`, () => { + const err = execAndGetErr(projectPath, './tsconfig.esm.json', 'esm'); + expect(err).toContain('To enable experimental ESM support, install the \'esm\' package'); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 175db1c..e82a65c 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,7 +4,7 @@ "compilerOptions": { "noEmit": true, - "target": "ES2018", + "target": "ESNext", "skipDefaultLibCheck": true, "skipLibCheck": true } diff --git a/tsconfig.base.json b/tsconfig.base.json index 96a7738..ca7ceca 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -7,9 +7,9 @@ "forceConsistentCasingInFileNames": true, "useUnknownInCatchVariables": false, - "lib": [ "es2019" ], + "lib": [ "es2020", "dom" ], "outDir": "dist", - "target": "es5", + "target": "ES2020", "module": "CommonJS", "moduleResolution": "node", diff --git a/yarn.lock b/yarn.lock index 8a525d8..cf5e121 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,163 +2,165 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.1.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== dependencies: - "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== dependencies: - "@babel/highlight" "^7.18.6" + "@babel/highlight" "^7.22.5" -"@babel/compat-data@^7.20.0": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" - integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== +"@babel/compat-data@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.5.tgz#b1f6c86a02d85d2dd3368a2b67c09add8cd0c255" + integrity sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA== "@babel/core@^7.11.6", "@babel/core@^7.12.3": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" - integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.5" - "@babel/parser" "^7.20.5" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.5.tgz#d67d9747ecf26ee7ecd3ebae1ee22225fe902a89" + integrity sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helpers" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.1" + json5 "^2.2.2" semver "^6.3.0" -"@babel/generator@^7.20.5", "@babel/generator@^7.7.2": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" - integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== +"@babel/generator@^7.22.5", "@babel/generator@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.5.tgz#1e7bf768688acfb05cf30b2369ef855e82d984f7" + integrity sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA== dependencies: - "@babel/types" "^7.20.5" + "@babel/types" "^7.22.5" "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/helper-compilation-targets@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" - integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== +"@babel/helper-compilation-targets@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz#fc7319fc54c5e2fa14b2909cf3c5fd3046813e02" + integrity sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw== dependencies: - "@babel/compat-data" "^7.20.0" - "@babel/helper-validator-option" "^7.18.6" + "@babel/compat-data" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" browserslist "^4.21.3" + lru-cache "^5.1.1" semver "^6.3.0" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-module-transforms@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" - integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== - -"@babel/helpers@^7.20.5": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" - integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz#0f65daa0716961b6e96b164034e737f60a80d2ef" + integrity sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz#88cf11050edb95ed08d596f7a044462189127a08" + integrity sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + +"@babel/helpers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.5.tgz#74bb4373eb390d1ceed74a15ef97767e63120820" + integrity sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" - integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" + integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -245,44 +247,44 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.7.2": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" - integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/template@^7.18.10", "@babel/template@^7.3.3": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" - integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.18.10" - "@babel/types" "^7.18.10" - -"@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.7.2": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" - integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.5" - "@babel/types" "^7.20.5" + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" + integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/template@^7.22.5", "@babel/template@^7.3.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.22.5", "@babel/traverse@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.5.tgz#44bd276690db6f4940fdb84e1cb4abd2f729ccd1" + integrity sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" - integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -511,38 +513,40 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== dependencies: "@jridgewell/set-array" "^1.0.1" "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": +"@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@1.4.14": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -551,21 +555,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== dependencies: "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@phenomnomnominal/tsquery@^4.1.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-4.2.0.tgz#7742ff4af12ce673b0b601ba5515c934f1876b14" - integrity sha512-hR2U3uVcrrdkuG30ItQ+uFDs4ncZAybxWG0OjTE8ptPzVoU7GVeXpy+vMU8zX9EbmjGeITPw/su5HjYQyAH8bA== - dependencies: - esquery "^1.0.1" - "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -601,17 +598,17 @@ integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== "@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/babel__core@^7.1.14": - version "7.1.20" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.20.tgz#e168cdd612c92a2d335029ed62ac94c95b362359" - integrity sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ== + version "7.20.1" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b" + integrity sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw== dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" "@types/babel__generator" "*" "@types/babel__template" "*" "@types/babel__traverse" "*" @@ -632,24 +629,31 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + version "7.20.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.1.tgz#dd6f1d2411ae677dcb2db008c962598be31d6acf" + integrity sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg== dependencies: - "@babel/types" "^7.3.0" + "@babel/types" "^7.20.7" -"@types/glob@*": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.0.0.tgz#321607e9cbaec54f687a0792b2d1d370739455d2" - integrity sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA== +"@types/esm@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@types/esm/-/esm-3.2.0.tgz#f53dbded33cc1cef95645ebbf287aa3b3742e103" + integrity sha512-aXemgVPnF1s0PQin04Ei8zTWaNwUdc4pmhZDg8LBW6QEl9kBWVItAUOLGUY5H5xduAmbL1pLGH1X/PN0+4R9tg== + dependencies: + "@types/node" "*" + +"@types/glob@~7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== dependencies: "@types/minimatch" "*" "@types/node" "*" "@types/graceful-fs@^4.1.3": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" - integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== dependencies: "@types/node" "*" @@ -680,19 +684,6 @@ expect "^28.0.0" pretty-format "^28.0.0" -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - -"@types/loader-utils@^1.1.3": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-1.1.6.tgz#41a6e6750ad1938e0498394d23459f9a62c5c73a" - integrity sha512-0U4S5kLpm3Cu9YkO46JrmujS2abL2tWsxA1SR8km6X0a1E96tfPu34zRdQSZsJ6dfRYwQpmuKmy9Mx2Od7AXag== - dependencies: - "@types/node" "*" - "@types/webpack" "^4" - "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -711,14 +702,14 @@ "@types/node" "*" "@types/node@*": - version "18.11.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.12.tgz#89e7f8aa8c88abf432f9bd594888144d7dba10aa" - integrity sha512-FgD3NtTAKvyMmD44T07zz2fEf+OKwutgBCEVM8GcvMGVGaDktiLNTDvPwC/LUe3PinMW+X6CuLOF2Ui1mAlSXg== + version "20.3.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe" + integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg== "@types/node@^16.11.5": - version "16.18.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.7.tgz#9e34404dba546a6f0851439ebba142680ae56fe0" - integrity sha512-SghuoXv8ghvkrKjTyvhRTeNzivPzGQ8pe09PPGdyqsExiKvBYV/6E3imvjsaJuW8ca61qQN2+SoSzyEHS9r2LA== + version "16.18.36" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.36.tgz#0db5d7efc4760d36d0d1d22c85d1a53accd5dc27" + integrity sha512-8egDX8dE50XyXWH6C6PRCNkTP106DuUrvdrednFouDSmCi7IOvrqr0frznfZaHifHH/3aq/7a7v9N4wdXMqhBQ== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -726,80 +717,42 @@ integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== "@types/prettier@^2.1.5": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" - integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== + version "2.7.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" + integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== "@types/resolve@^1.20.1": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== +"@types/semver@^7.3.13": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + "@types/shelljs@^0.8.9": - version "0.8.11" - resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.11.tgz#17a5696c825974e96828e96e89585d685646fcb8" - integrity sha512-x9yaMvEh5BEaZKeVQC4vp3l+QoFj3BXcd4aYfuKSzIIyihjdVARAadYy3SMNIz0WCCdS2vB9JL/U6GQk5PaxQw== + version "0.8.12" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.12.tgz#79dc9632af7d5ca1b5afb65a6bfc1422d79b5fa0" + integrity sha512-ZA8U81/gldY+rR5zl/7HSHrG2KDfEb3lzG6uCUDhW1DTQE9yC/VBQ45fXnXq8f3CgInfhZmjtdu/WOUlrXRQUg== dependencies: - "@types/glob" "*" + "@types/glob" "~7.2.0" "@types/node" "*" -"@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== - "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/tapable@^1": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" - integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== - -"@types/ts-expose-internals@npm:ts-expose-internals@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/ts-expose-internals/-/ts-expose-internals-4.4.4.tgz#956ddba72382434afdbd28c539ff8695a519a548" - integrity sha512-pZI+Jqb6dhfTACWBg9ahcVCYAX5s1KBCLm0FpwwhKxCvEFvmSLaMpTbyVf1h5pdCh2DS5uNPwG4AH+Vvqri9/g== - -"@types/uglify-js@*": - version "3.17.1" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.1.tgz#e0ffcef756476410e5bce2cb01384ed878a195b5" - integrity sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g== - dependencies: - source-map "^0.6.1" - -"@types/webpack-sources@*": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b" - integrity sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.7.3" - -"@types/webpack@^4": - version "4.41.33" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.33.tgz#16164845a5be6a306bcbe554a8e67f9cac215ffc" - integrity sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g== - dependencies: - "@types/node" "*" - "@types/tapable" "^1" - "@types/uglify-js" "*" - "@types/webpack-sources" "*" - anymatch "^3.0.0" - source-map "^0.6.0" - "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.17" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.17.tgz#5672e5621f8e0fca13f433a8017aae4b7a2a03e7" - integrity sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g== + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== dependencies: "@types/yargs-parser" "*" @@ -817,9 +770,9 @@ acorn-walk@^8.1.1: integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== acorn@^8.4.1: - version "8.8.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" - integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== add-stream@^1.0.0: version "1.0.0" @@ -857,7 +810,7 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -anymatch@^3.0.0, anymatch@^3.0.3: +anymatch@^3.0.3: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -952,11 +905,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -965,13 +913,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -980,14 +921,14 @@ braces@^3.0.2: fill-range "^7.0.1" browserslist@^4.21.3: - version "4.21.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" - integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + version "4.21.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.8.tgz#db2498e1f4b80ed199c076248a094935860b6017" + integrity sha512-j+7xYe+v+q2Id9qbBeCI8WX5NmZSRe8es1+0xntD/+gaWXznP8tFEkv5IgSaHf5dS1YwVMbX/4W6m937mj+wQw== dependencies: - caniuse-lite "^1.0.30001400" - electron-to-chromium "^1.4.251" - node-releases "^2.0.6" - update-browserslist-db "^1.0.9" + caniuse-lite "^1.0.30001502" + electron-to-chromium "^1.4.428" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" bs-logger@0.x: version "0.2.6" @@ -1032,10 +973,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001400: - version "1.0.30001436" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz#22d7cbdbbbb60cdc4ca1030ccd6dea9f5de4848b" - integrity sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg== +caniuse-lite@^1.0.30001502: + version "1.0.30001502" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001502.tgz#f7e4a76eb1d2d585340f773767be1fefc118dca8" + integrity sha512-AZ+9tFXw1sS0o0jcpJQIXvFTOB/xGiQ4OQ2t98QX3NDn2EZTSRBC801gxrsGgViuq2ak/NLkNgSNEPtCr5lfKg== chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" @@ -1060,14 +1001,14 @@ char-regex@^1.0.2: integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== ci-info@^3.2.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" - integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== cjs-module-lexer@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" - integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== cliui@^7.0.2: version "7.0.4" @@ -1367,9 +1308,9 @@ dedent@^0.7.0: integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== detect-indent@^6.0.0: version "6.1.0" @@ -1406,10 +1347,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -electron-to-chromium@^1.4.251: - version "1.4.284" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" - integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== +electron-to-chromium@^1.4.428: + version "1.4.429" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.429.tgz#055a2543ba8b1a847bebf521791715ec30f8d97b" + integrity sha512-COua8RvN548KwPFzKMrTjFbmDsQRgdi0zSAhmo70TwC1tfLOSqq8p09n+GkdF5buvzE/NEYn1dP3itbfhun9gg== emittery@^0.10.2: version "0.10.2" @@ -1421,11 +1362,6 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -1448,23 +1384,16 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -estraverse@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -1631,7 +1560,7 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: +glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1643,17 +1572,6 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" - integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - global-prefix@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" @@ -1669,21 +1587,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== graceful-fs@^4.1.2, graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -graphql-tag@^2.10.3: - version "2.12.6" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" - integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== - dependencies: - tslib "^2.1.0" - -graphql@^15.4.0: - version "15.8.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" - integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== handlebars@^4.7.7: version "4.7.7" @@ -1787,10 +1693,10 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-core-module@^2.5.0, is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== +is-core-module@^2.11.0, is-core-module@^2.5.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== dependencies: has "^1.0.3" @@ -2279,17 +2185,10 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +json5@^2.2.1, json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonparse@^1.2.0: version "1.3.1" @@ -2326,15 +2225,6 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" -loader-utils@^1.1.0, loader-utils@^1.2.3: - version "1.4.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" - integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -2380,6 +2270,13 @@ lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -2463,13 +2360,6 @@ minimatch@^3.0.4, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.1.tgz#6c9dffcf9927ff2a31e74b5af11adf8b9604b022" - integrity sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g== - dependencies: - brace-expansion "^2.0.1" - minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -2479,15 +2369,10 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== - -mock-fs@^5.1.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.2.0.tgz#3502a9499c84c0a1218ee4bf92ae5bf2ea9b2b5e" - integrity sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw== +minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== modify-values@^1.0.0: version "1.0.1" @@ -2514,10 +2399,10 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-releases@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" - integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== +node-releases@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" + integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" @@ -2781,18 +2666,18 @@ read-pkg@^5.2.0: type-fest "^0.6.0" readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -2835,24 +2720,19 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve.exports@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" - integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" + integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.11.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -2875,10 +2755,10 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.x, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== +semver@7.x, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8: + version "7.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== dependencies: lru-cache "^6.0.0" @@ -2936,15 +2816,10 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" @@ -2963,9 +2838,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.12" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779" - integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== + version "3.0.13" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5" + integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== split2@^3.0.0: version "3.2.2" @@ -3195,7 +3070,7 @@ ts-jest@^28.0.7: semver "7.x" yargs-parser "^21.0.1" -ts-node@^10.4.0: +ts-node@^10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== @@ -3214,52 +3089,27 @@ ts-node@^10.4.0: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -ts-patch@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ts-patch/-/ts-patch-2.0.2.tgz#be9dc294ced6f53d6a8912f1f6ac98af23a715cd" - integrity sha512-NbgdS/J/ylaARJVaF1w4cPw378yvw6C1026fU5NKC2GO4jCwRlkuE/G7gwknNMHwkAOhwamKjuzkuLw/u4KlBg== +ts-patch@3.0.0-rc2: + version "3.0.0-rc2" + resolved "https://registry.yarnpkg.com/ts-patch/-/ts-patch-3.0.0-rc2.tgz#6b9d9ae3704fbf32698caa83f8979b33a5f88cf6" + integrity sha512-dsu+3k1cKJ1HVQlCimddJxRfq48XBAQHGAmLyKrIn8pGzz9dhTGsjo/bQFUDicVD2KZB72U+aN82sGkqqThEnQ== dependencies: chalk "^4.1.2" - glob "^8.0.3" global-prefix "^3.0.0" - minimist "^1.2.6" - resolve "^1.22.1" - shelljs "^0.8.5" + minimist "^1.2.8" + resolve "^1.22.2" + semver "^7.3.8" strip-ansi "^6.0.1" -ts-transform-img@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/ts-transform-img/-/ts-transform-img-0.4.2.tgz#5ae2550e033ecd52ad1d39a2b86ece0d307cefd2" - integrity sha512-KuE+rZeRP7wtWHWn+N167oM/YrobMiqkOG5Pw7RYd917liYJXgymocP+Fc0qbpudhhAcNMnKjsixkkbUInxx0w== - dependencies: - "@types/loader-utils" "^1.1.3" - loader-utils "^1.2.3" - typescript "^3.2" - -ts-transform-react-intl@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/ts-transform-react-intl/-/ts-transform-react-intl-0.4.1.tgz#ad262268960de230d8ed5eea999d846cab4d8835" - integrity sha512-/v1GUBUt+YBRFIuV4Jo006+2UWkWjJLRAuxHmaMkcY4246UZXfXE5FSVncB4ILUjaEQyNDB8lrv4iq+qiPYh7g== - dependencies: - "@types/loader-utils" "^1.1.3" - loader-utils "^1.1.0" - typescript "3" - -tsconfig-paths@^3.11.0: - version "3.14.1" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" - integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" + json5 "^2.2.2" minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -3290,25 +3140,20 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@3, typescript@^3.2: - version "3.9.10" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" - integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== - -typescript@4.8.4: - version "4.8.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +typescript@^5.0.4: + version "5.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826" + integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw== uglify-js@^3.1.4: version "3.17.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== -update-browserslist-db@^1.0.9: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -3324,9 +3169,9 @@ v8-compile-cache-lib@^3.0.1: integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== v8-to-istanbul@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" - integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" @@ -3398,6 +3243,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -3427,9 +3277,9 @@ yargs@^16.0.0, yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.3.1: - version "17.6.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1"