├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE.md ├── README.md ├── docs └── zh.md ├── images ├── code-action.png ├── commands.png ├── cover.png ├── demo.gif ├── logo.png ├── samples.gif └── title-logo.png ├── packages ├── extension │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscodeignore │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── __snapshots__ │ │ │ └── extension.test.ts.snap │ │ ├── code-action.ts │ │ ├── commands │ │ │ ├── addATypehole.ts │ │ │ ├── removeTypeholesFromAllFiles.ts │ │ │ └── removeTypeholesFromCurrentFile.ts │ │ ├── config.ts │ │ ├── diagnostics.ts │ │ ├── editor │ │ │ └── utils.ts │ │ ├── ensureRuntime.ts │ │ ├── extension.test.ts │ │ ├── extension.ts │ │ ├── hole.ts │ │ ├── listener.ts │ │ ├── logger.ts │ │ ├── parse │ │ │ ├── expression.test.ts │ │ │ ├── expression.ts │ │ │ ├── module.test.ts │ │ │ ├── module.ts │ │ │ └── utils.ts │ │ ├── state.ts │ │ └── transforms │ │ │ ├── __snapshots__ │ │ │ └── wrapIntoRecorder.test.ts.snap │ │ │ ├── insertTypes │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── samplesToType │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.ts.snap │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ └── redditResponse.json │ │ │ ├── wrapIntoRecorder.test.ts │ │ │ └── wrapIntoRecorder.ts │ ├── tsconfig.json │ └── vsc-extension-quickstart.md └── runtime │ ├── .editorconfig │ ├── .github │ └── workflows │ │ └── ci.yml │ ├── .gitignore │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── rollup.config.js │ ├── src │ └── index.ts │ ├── test │ └── index.ts │ └── tsconfig.json └── publish /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | /package.json 4 | /package-lock.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}/packages/extension" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/packages/extension/out/**/*.js" 17 | ] 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "args": [ 24 | "--extensionDevelopmentPath=${workspaceFolder}/packages/extension", 25 | "--extensionTestsPath=${workspaceFolder}/packages/extension/out/test/suite/index" 26 | ], 27 | "outFiles": [ 28 | "${workspaceFolder}/packages/extension/out/test/**/*.js" 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Riku Rouvila 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typehle 2 | 3 | Automatically generate TypeScript types and interfaces for all serializable runtime values. 4 | 5 | [English](#) | [简体中文](./docs/zh.md) 6 | 7 | Typehole is a TypeScript development tool for Visual Studio Code that automates creating static typing by bridging runtime values from your Node.js or browser application to your code editor. It's useful when you need types for an API response or want to figure out types for values coming from a JS module. 8 |
9 |
10 | 11 | ![file](./images/demo.gif) 12 | 13 | ## Installation 14 | 15 | Install the [Visual Studio Code - extension](https://marketplace.visualstudio.com/items?itemName=rikurouvila.typehole). No additional build tooling or compiler plugins are needed. 16 | 17 | ## How does it work? 18 | 19 | 1. Find an `any` / `unknown` value you need an interface for 20 | 21 | ```typescript 22 | const response = await axios.get("https://reddit.com/r/typescript.json"); 23 | const data /* any */ = response.data; 24 | ``` 25 | 26 | 2. Place the value inside a typehole by selecting an expression and opening the **Quick Fix** menu by pressing ⌘ + . (macOS) or ctrl + . (Windows) and selecting **Add a typehole**. 27 | 28 | ```typescript 29 | type RedditResponse = any; // Type placeholder inserted by the extension 30 | const response = await axios.get("https://reddit.com/r/typescript.json"); 31 | 32 | const data: RedditResponse = typehole.t(response.data); 33 | ``` 34 | 35 | 3. Run your code either in a browser or in Node.js. Typehole runtime captures the value and sends it back to your code editor. The VSCode extension records the captured value, turns all the values from that typehole into an interface and inserts it into the same module. 36 | 37 | ```typescript 38 | interface RedditResponse { 39 | /* ✨ Actual fields and types are automatically generated ✨ */ 40 | } 41 | 42 | const response = await axios.get("https://reddit.com/r/typescript.json"); 43 | const data: RedditResponse = typehole.t(response.data); 44 | ``` 45 | 46 | 4. Remove the typehole, and you're done. Typeholes are meant to be development-time only, so you shouldn't commit them. Typehole provides you with 2 [commands](#commands) for easy removal of typeholes. 47 | 48 | ```typescript 49 | interface RedditResponse { 50 | /* ✨ Actual fields and types are automatically generated ✨ */ 51 | } 52 | 53 | const response = await axios.get("https://reddit.com/r/typescript.json"); 54 | const data: RedditResponse = response.data; 55 | ``` 56 | 57 | This plugin is still very experimental, so please expect and report issues. 58 | 59 | ## Features 60 | 61 | - Generate TypeScript types from runtime values 62 | - Run the code many times with different values thus augmenting your types

63 | 64 | - Wrap values automatically to typeholes with a code action

65 | 66 | ### Values that can be automatically typed 67 | 68 | All primitive values and values that are JSON serializable. 69 | 70 | - Booleans 71 | - Numbers 72 | - Strings 73 | - Arrays 74 | - Objects 75 | - null 76 | 77 | So all values you can receive as an HTTP request payload can be turned into an interface. 78 | 79 | From 1.4.0 forward also Promises are supported. All other values (functions etc.) will be typed as `any`. 80 | 81 | ## Commands 82 | 83 | ![image](./images/commands.png) 84 | 85 | - Starting and stopping the server manually isn't necessary by default. The server starts once you add your first typehole. 86 | 87 | ## Extension Settings 88 | 89 | | Setting | Type | Default | Description | 90 | | ------------------------------- | ----------------- | --------- | ------------------------------------------------------------------------------- | 91 | | typehole.runtime.autoInstall | boolean | true | Install Typehole runtime package automatically when the first typehole is added | 92 | | typehole.runtime.projectPath | string | | Project directory where Typehole runtime should be installed | 93 | | typehole.runtime.packageManager | npm \| yarn | npm | Package manager to be used when installing the runtime | 94 | | typehole.runtime.extensionPort | number | 17341 | HTTP port for HTTP extension to listen for incoming samples | 95 | | typehole.typeOrInterface | interface \| type | interface | Keyword to be used for generated types | 96 | 97 | ## Runtime 98 | 99 | Typehole runtime's job is to capture values in your code and to send them to the extension in a serialized format. 100 | 101 | ```typescript 102 | import typehole from "typehole"; 103 | 104 | // -> POST http://extension/samples {"id": "t", "sample": "value"} 105 | typehole.t("value"); 106 | 107 | // -> POST http://extension/samples {"id": "t1", "sample": 23423.432} 108 | typehole.t1(23423.432); 109 | 110 | // -> POST http://extension/samples {"id": "t2", "sample": {"some": "value"}} 111 | typehole.t2({ some: "value" }); 112 | ``` 113 | 114 | Typeholes are identified by the method name of your typehole call. Call `.t2()` would give the hole an id "t2". The ids are there, so the extension knows from where the value is coming from in the code. 115 | 116 | In most cases, you should use unique keys for all holes. However, if you wish to record values from many holes into the same type, you might use the same id. 117 | 118 | In some cases, the extension might not be running on the same host as your code, and you want to configure the address where the runtime sends the values. Node.js application running inside of a Docker container is one such case. In most cases, however, you do not need to configure anything. 119 | 120 | ```typescript 121 | import typehole, { configure } from "typehole"; 122 | 123 | configure({ 124 | extensionHost: "http://host.docker.internal:17341", 125 | }); 126 | ``` 127 | 128 | ### Available runtime settings 129 | 130 | | Setting | Type | Default | Description | 131 | | ------------- | ------ | ---------------------- | ----------------------------------------------------------- | 132 | | extensionHost | string | http://localhost:17341 | The address in which the extension HTTP listener is running | 133 | 134 | ## Known Issues 135 | 136 | - Typehole server cannot be running in 2 VSCode editors at the same time as the server port is hard-coded to 17341 137 | 138 | ## Release Notes 139 | 140 | ## [1.8.0] - 2023-03-14 141 | 142 | ### Added 143 | 144 | - Add native NodeJS ESM modules support [#24](https://github.com/rikukissa/typehole/pull/24) 145 | 146 | ## [1.7.0] - 2021-07-08 147 | 148 | ### Added 149 | 150 | - New option "typehole.typeOrInterface" added for using `type` keyword instead of `interface`. All thanks to @akafaneh 🎉 151 | 152 | ## [1.6.3] - 2021-06-20 153 | 154 | ### Fixed 155 | 156 | - Fixes code formatting generating broken / duplicate code 157 | 158 | ## [1.6.2] - 2021-05-22 159 | 160 | ### Fixed 161 | 162 | - Fixes null values marking fields as optional. `[{"foo": null}, {"foo": 2}]` now generates a type `{foo: null | number}[]` and not `{foo?: number}[]` like it used to. Should fix [#14](https://github.com/rikukissa/typehole/issues/14) 163 | 164 | ## [1.6.1] - 2021-05-22 165 | 166 | ### Fixed 167 | 168 | - Fix the automatic formatting in files where types are inserted 169 | 170 | ## [1.6.0] - 2021-05-20 171 | 172 | ### Added 173 | 174 | - Options for configuring both the extension server port and runtime host address. Addresses [#13](https://github.com/rikukissa/typehole/issues/13) 175 | 176 | ## [1.5.1] - 2021-05-18 177 | 178 | ### Fixed 179 | 180 | - Multiple typeholes can now exist with the same id. Each update from all of them updates all types attached to the holes. Useful, for example, when you want to have multiple typeholes update the same type. 181 | - No duplicated interfaces anymore when the generated top-level type is a `ParenthesizedType` 182 | - Interface not updating when it was in a different file than the typehole 183 | - Types not updating when some other file was focused in the editor 184 | - `typehole.tNaN` [issue](https://github.com/rikukissa/typehole/issues/7) when there have been typeholes with a non `t` format 185 | 186 | ## [1.5.0] - 2021-05-15 187 | 188 | ### Added 189 | 190 | - Support for inferring Promises 👀 191 | 192 | ### Fixed 193 | 194 | - Runtime now installed also on startup if there are typeholes in your code 195 | - No more duplicate AutoDiscoveredN types 196 | 197 | ## [1.4.1] - 2021-05-09 198 | 199 | ### Fixed 200 | 201 | - Unserializable diagnostic now shown only once per typehole. Previously the tooltip could have the same warning multiple times. 202 | 203 | - Server is now stopped once all typeholes are removed. Restarting the server now also works 204 | 205 | ### Added 206 | 207 | ## [1.4.0] - 2021-05-09 208 | 209 | ### Added 210 | 211 | - Sample collection. Provide multiple different values to a typehole and the generated type gets refined based on them. 212 | 213 | ## [1.3.0] - 2021-05-08 214 | 215 | ### Added 216 | 217 | - Configuration options for project path, package manager and if runtime should be automatically installed 218 | 219 | ## [1.1.0] - 2021-05-08 220 | 221 | ### Added 222 | 223 | - Automatic PascalCase transformation for all generated interface and type alias names 224 | 225 | --- 226 | 227 | **Enjoy!** 228 | -------------------------------------------------------------------------------- /docs/zh.md: -------------------------------------------------------------------------------- 1 | # Typehle 2 | 3 | 为所有运行时可序列化的值自动生成 Typescript 类型和接口 4 | 5 | [English](../README.md) | [简体中文](#) 6 | 7 | Typehole 是 Visual Studio Code 的 TypeScript 开发工具,它通过将运行时的值从 Node.js 或浏览器应用程序中桥接到代码编辑器来自动创建静态类型。当您需要 API 响应的类型或想要得到来自 JS 模块值的类型时,它是非常有用的。 8 |
9 |
10 | 11 | ![file](../images/demo.gif) 12 | 13 | ## 安装 14 | 15 | 安装 [Visual Studio Code - extension](https://marketplace.visualstudio.com/items?itemName=rikurouvila.typehole) 即可,不需要额外的构建工具或编译器插件。 16 | 17 | ## 它是如何工作的? 18 | 19 | 1. 从一个接口中获得 `any` / `unknown` 类型的值的类型。 20 | 21 | ```typescript 22 | const response = await axios.get("https://reddit.com/r/typescript.json"); 23 | const data /* any */ = response.data; 24 | ``` 25 | 26 | 2. 通过选择表达式并按 ⌘ + 打开 **Quick Fix** 菜单,将值放置在 typeholes 中。 (macOS) 或 ctrl + . (Windows) 并选择 **Add a typehole**。 27 | 28 | ```typescript 29 | type RedditResponse = any; // 由扩展插入的类型占位符 30 | const response = await axios.get("https://reddit.com/r/typescript.json"); 31 | 32 | const data: RedditResponse = typehole.t(response.data); 33 | ``` 34 | 35 | 3. 在浏览器或 Node.js 中运行您的代码。 Typehole 会在运行时捕获该值并将其发送回您的代码编辑器。VSCode 扩展会记录捕获的值,将来自该 typehole 的所有值转换为一个 interface 并将其插入到同一个模块中。 36 | 37 | ```typescript 38 | interface RedditResponse { 39 | /* ✨ 实际的字段和类型是自动生成的 ✨ */ 40 | } 41 | 42 | const response = await axios.get("https://reddit.com/r/typescript.json"); 43 | const data: RedditResponse = typehole.t(response.data); 44 | ``` 45 | 46 | 4. 移除 typehole,就完成了所有的操作。 Typeholes 仅用于开发阶段,所以您不应该提交它们。 Typehole 为您提供了 2 个 [命令](#命令) 来轻松移除 typehole 47 | 48 | ```typescript 49 | interface RedditResponse { 50 | /* ✨ 实际的字段和类型是自动生成的 ✨ */ 51 | } 52 | 53 | const response = await axios.get("https://reddit.com/r/typescript.json"); 54 | const data: RedditResponse = response.data; 55 | ``` 56 | 57 | 这个插件任然是实验性质的,如有问题请反馈 issues 58 | 59 | ## 特性 60 | 61 | - 从运行中的值生成 Typescript 类型 62 | - 使用不同的值多次运行代码,从而增加您的类型

63 | 64 | - 使用代码操作将值自动包装到 typeholes

65 | 66 | ### 值能够自动的被转换为类型 67 | 68 | 所有原始值和 JSON 可序列化的值。 69 | 70 | - Booleans 71 | - Numbers 72 | - Strings 73 | - Arrays 74 | - Objects 75 | - null 76 | 77 | 因此,您可以其作为 HTTP 请求有效负载,接收的所有值都可以转换为 interface。 78 | 79 | 从 1.4.0 开始,支持 Promise。所有其他值(函数等)将被输入为 `any`。 80 | 81 | ## 命令 82 | 83 | ![image](../images/commands.png) 84 | 85 | - 默认情况下不需要手动启动和停止服务器。 添加第一个 typehole 后,服务器将启动。 86 | 87 | ## 扩展设置 88 | 89 | | 设置 | 类型 | 默认值 | 描述 | 90 | | ------------------------------- | ----------------- | --------- | ------------------------------------------------ | 91 | | typehole.runtime.autoInstall | boolean | true | 添加第一个 typehole 时自动安装 Typehole 运行时包 | 92 | | typehole.runtime.projectPath | string | | 安装 Typehole 运行时的项目目录 | 93 | | typehole.runtime.packageManager | npm \| yarn | npm | 安装运行时使用的包管理器 | 94 | | typehole.runtime.extensionPort | number | 17341 | 监听传入示例的 HTTP 扩展的 HTTP 端口 | 95 | | typehole.typeOrInterface | interface \| type | interface | 生成类型的关键字 | 96 | 97 | ## 运行时 98 | 99 | Typehole 运行时的工作是捕获代码中的值,并将它们以序列化格式发送给扩展。 100 | 101 | ```typescript 102 | import typehole from "typehole"; 103 | 104 | // -> POST http://extension/samples {"id": "t", "sample": "value"} 105 | typehole.t("value"); 106 | 107 | // -> POST http://extension/samples {"id": "t1", "sample": 23423.432} 108 | typehole.t1(23423.432); 109 | 110 | // -> POST http://extension/samples {"id": "t2", "sample": {"some": "value"}} 111 | typehole.t2({ some: "value" }); 112 | ``` 113 | 114 | typehole 是通过您的 typehole 调用的方法名来识别的。 调用 `.t2()` 的时候会给这个 hole 一个 id "t2".因为 ids 的存在, 所以扩展知道值来自代码中的什么地方。 115 | 116 | 大部分情况下, 你应该为所有的 holes 使用唯一的 id. 然而, 如果您希望将许多 holes 中的值记录到同一类型中,您可以使用相同的 id。 117 | 118 | 有时候, 扩展可能与您的代码不在同一台主机上运行, 你想配置运行时发送值的地址。 在 Docker 容器内运行的 Node.js 应用程序就是这样一种情况。但是,在大多数情况下,您不需要配置任何内容。 119 | 120 | ```typescript 121 | import typehole, { configure } from "typehole"; 122 | 123 | configure({ 124 | extensionHost: "http://host.docker.internal:17341", 125 | }); 126 | ``` 127 | 128 | ### 可用的运行时设置 129 | 130 | | 设置 | 类型 | 默认值 | 描述 | 131 | | ------------- | ------ | ---------------------- | -------------------------- | 132 | | extensionHost | string | http://localhost:17341 | 扩展 HTTP 监听器的运行地址 | 133 | 134 | ## 已知问题 135 | 136 | - Typehole 服务器不能在 2 个 VSCode 编辑器中同时运行,因为服务器端口硬编码为 17341 137 | 138 | ## 发行说明 139 | 140 | ## [1.8.0] - 2023-03-14 141 | 142 | ## [1.7.0] - 2021-07-08 143 | 144 | ### Added 145 | 146 | - 新选项”typehole.typeOrInterface"添加用于使用' type '关键字而不是' interface '。 这一切都归功于 @akafaneh 🎉 147 | 148 | ## [1.6.3] - 2021-06-20 149 | 150 | ### Fixed 151 | 152 | - 修复代码格式生成损坏/重复的代码 153 | 154 | ## [1.6.2] - 2021-05-22 155 | 156 | ### Fixed 157 | 158 | - 修复了将字段标记为可选的空值。 `[{"foo": null}, {"foo": 2}]` 现在生成一个 type `{foo: null | number}[]` 而不是像以前一样生成 `{foo?: number}[]`. 应该被修复 [#14](https://github.com/rikukissa/typehole/issues/14) 159 | 160 | ## [1.6.1] - 2021-05-22 161 | 162 | ### Fixed 163 | 164 | - 修复插入了类型的文件的自动格式化 165 | 166 | ## [1.6.0] - 2021-05-20 167 | 168 | ### Added 169 | 170 | - 用于配置扩展服务器端口和运行时主机地址的选项。 地址 [#13](https://github.com/rikukissa/typehole/issues/13) 171 | 172 | ## [1.5.1] - 2021-05-18 173 | 174 | ### Fixed 175 | 176 | - 多个 typeholes 可以使用同一个 id。 它们的每一次更新都会更新附加到孔上的所有类型。 例如,当您希望有多个 typeholes 更新相同的类型时,这很有用。 177 | - 当生成的顶层类型是一个 `ParenthesizedType` 的时候,不会再有重复的 interfaces。 178 | - 当 interface 和 typehole 不在同一个文件的时候,interface 不会更新。 179 | - 当编辑器中聚焦其他文件时,类型不会更新。 180 | - `typehole.tNaN` [issue](https://github.com/rikukissa/typehole/issues/7) 当有非`t`格式的 typeholes 的时候 181 | 182 | ## [1.5.0] - 2021-05-15 183 | 184 | ### Added 185 | 186 | - 支持推断 Promises 👀 187 | 188 | ### Fixed 189 | 190 | - 如果你的代码中有 typehole,那么 runtime 也会在启动时安装 191 | - AutoDiscoveredN 类型不再重复 192 | 193 | ## [1.4.1] - 2021-05-09 194 | 195 | ### Fixed 196 | 197 | - 非序列化的诊断现在每个 typehole 只显示一次。 以前,工具提示可能有多次相同的警告。 198 | 199 | - 删除所有的 typeholes 后,服务器会停止。重新启动服务器现在也可以工作。 200 | 201 | ### Added 202 | 203 | ## [1.4.0] - 2021-05-09 204 | 205 | ### Added 206 | 207 | - 样本收集。 为一个 typehole 提供多个不同的值,生成的类型将基于这些值进行优化。 208 | 209 | ## [1.3.0] - 2021-05-08 210 | 211 | ### Added 212 | 213 | - 项目路径、包管理器和是否应该自动安装运行时的配置选项 214 | 215 | ## [1.1.0] - 2021-05-08 216 | 217 | ### Added 218 | 219 | - 所有生成的接口和类型别名的自动 PascalCase 转换 220 | 221 | --- 222 | 223 | **尽情畅享!** 224 | -------------------------------------------------------------------------------- /images/code-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikukissa/typehole/96bf313bb800e81b7fa887ba1e1572ecc9d6d820/images/code-action.png -------------------------------------------------------------------------------- /images/commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikukissa/typehole/96bf313bb800e81b7fa887ba1e1572ecc9d6d820/images/commands.png -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikukissa/typehole/96bf313bb800e81b7fa887ba1e1572ecc9d6d820/images/cover.png -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikukissa/typehole/96bf313bb800e81b7fa887ba1e1572ecc9d6d820/images/demo.gif -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikukissa/typehole/96bf313bb800e81b7fa887ba1e1572ecc9d6d820/images/logo.png -------------------------------------------------------------------------------- /images/samples.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikukissa/typehole/96bf313bb800e81b7fa887ba1e1572ecc9d6d820/images/samples.gif -------------------------------------------------------------------------------- /images/title-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikukissa/typehole/96bf313bb800e81b7fa887ba1e1572ecc9d6d820/images/title-logo.png -------------------------------------------------------------------------------- /packages/extension/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "**/*.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/extension/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | images 7 | README.md -------------------------------------------------------------------------------- /packages/extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | -------------------------------------------------------------------------------- /packages/extension/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testMatch: ["**/*.test.ts"], 5 | }; 6 | -------------------------------------------------------------------------------- /packages/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typehole", 3 | "displayName": "Typehole", 4 | "publisher": "rikurouvila", 5 | "description": "🧪 Take samples of runtime values and turn them into type definitions automatically", 6 | "repository": "https://github.com/rikukissa/typehole", 7 | "version": "1.8.0", 8 | "private": true, 9 | "icon": "images/logo.png", 10 | "galleryBanner": { 11 | "color": "#222145", 12 | "theme": "dark" 13 | }, 14 | "engines": { 15 | "vscode": "^1.55.0" 16 | }, 17 | "categories": [ 18 | "Other" 19 | ], 20 | "activationEvents": [ 21 | "*" 22 | ], 23 | "main": "./out/extension.js", 24 | "contributes": { 25 | "configuration": [ 26 | { 27 | "title": "Typehole", 28 | "properties": { 29 | "typehole.runtime.autoInstall": { 30 | "type": "boolean", 31 | "default": true, 32 | "description": "Installs Typehole runtime package automatically when the first typehole is added", 33 | "scope": "window" 34 | }, 35 | "typehole.typeOrInterface": { 36 | "type": "string", 37 | "enum": [ 38 | "type", 39 | "interface" 40 | ], 41 | "default": "interface", 42 | "description": "Keyword to be used for generated types", 43 | "scope": "window" 44 | }, 45 | "typehole.runtime.extensionPort": { 46 | "type": "number", 47 | "default": 17341, 48 | "description": "HTTP port for HTTP extension to listen for incoming samples", 49 | "scope": "window" 50 | }, 51 | "typehole.runtime.projectPath": { 52 | "type": "string", 53 | "description": "Location where Typehole runtime package is installed", 54 | "scope": "window" 55 | }, 56 | "typehole.runtime.packageManager": { 57 | "type": "string", 58 | "default": "npm", 59 | "enum": [ 60 | "npm", 61 | "yarn" 62 | ], 63 | "description": "Package manager to use when installing the runtime", 64 | "scope": "window" 65 | } 66 | } 67 | } 68 | ], 69 | "commands": [ 70 | { 71 | "title": "Typehole: Remove all typeholes from all files", 72 | "command": "typehole.remove-from-all-files" 73 | }, 74 | { 75 | "title": "Typehole: Remove all typeholes from current file", 76 | "command": "typehole.remove-from-current-file" 77 | }, 78 | { 79 | "title": "Typehole: Stop server", 80 | "command": "typehole.stop-server" 81 | }, 82 | { 83 | "title": "Typehole: Start server", 84 | "command": "typehole.start-server" 85 | } 86 | ] 87 | }, 88 | "scripts": { 89 | "vscode:prepublish": "cp -r ../../README.md ../../images . && npm run compile", 90 | "publish-extension": "vsce package && vsce publish", 91 | "compile": "tsc -p ./", 92 | "watch": "tsc -watch -p ./", 93 | "lint": "eslint src --ext ts", 94 | "test": "jest" 95 | }, 96 | "devDependencies": { 97 | "@types/glob": "^7.1.3", 98 | "@types/jest": "^26.0.22", 99 | "@types/node": "^12.11.7", 100 | "@types/pascalcase": "^1.0.0", 101 | "@types/vscode": "^1.55.0", 102 | "@typescript-eslint/eslint-plugin": "^4.14.1", 103 | "@typescript-eslint/parser": "^4.14.1", 104 | "eslint": "^7.19.0", 105 | "glob": "^7.1.6", 106 | "jest": "^26.6.3", 107 | "ts-jest": "^26.5.5", 108 | "vscode-test": "^1.5.0" 109 | }, 110 | "dependencies": { 111 | "@phenomnomnominal/tsquery": "^4.1.1", 112 | "@riku/json-to-ts": "^2.1.0", 113 | "@types/esquery": "^1.0.1", 114 | "@types/npm": "^2.0.31", 115 | "esquery": "^1.4.0", 116 | "fastify": "^3.14.2", 117 | "fastify-cors": "^5.2.0", 118 | "lmify": "^0.3.0", 119 | "npm": "^7.10.0", 120 | "pascalcase": "^0.1.1", 121 | "ts-morph": "^10.0.2", 122 | "typescript": "^4.1.3" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/extension/src/__snapshots__/extension.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Finds all dependency type nodes from an AST 1`] = ` 4 | Array [ 5 | "type Something = { 6 | a: B; 7 | };", 8 | "type B = { 9 | moi: C | D; 10 | };", 11 | "type C = 2;", 12 | "type D = 3;", 13 | ] 14 | `; 15 | -------------------------------------------------------------------------------- /packages/extension/src/code-action.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { isValidSelection } from "./parse/expression"; 3 | import { getAST } from "./parse/module"; 4 | import { 5 | getDescendantAtRange, 6 | lineCharacterPositionInText, 7 | } from "./parse/utils"; 8 | 9 | export class TypeHoler implements vscode.CodeActionProvider { 10 | public static readonly providedCodeActionKinds = [ 11 | vscode.CodeActionKind.QuickFix, 12 | ]; 13 | 14 | public provideCodeActions( 15 | document: vscode.TextDocument, 16 | range: vscode.Range 17 | ): vscode.ProviderResult { 18 | const fullFile = document.getText(); 19 | 20 | const startPosition = lineCharacterPositionInText(range.start, fullFile); 21 | const endPosition = lineCharacterPositionInText(range.end, fullFile); 22 | 23 | const selectedNode = getDescendantAtRange(getAST(fullFile), [ 24 | startPosition, 25 | endPosition, 26 | ]); 27 | 28 | if (!isValidSelection(selectedNode)) { 29 | return; 30 | } 31 | 32 | return [ 33 | { 34 | command: "typehole.add-a-typehole", 35 | title: "Add a typehole", 36 | }, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/extension/src/commands/addATypehole.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as vscode from "vscode"; 3 | 4 | import { getEditorRange } from "../editor/utils"; 5 | import { 6 | getPlaceholderTypeName, 7 | insertRecorderToSelection, 8 | insertTypeholeImport, 9 | last, 10 | startRenamingPlaceholderType, 11 | } from "../extension"; 12 | import { 13 | findTypeholes, 14 | getAST, 15 | getNodeEndPosition, 16 | getNodeStartPosition, 17 | getParentOnRootLevel, 18 | } from "../parse/module"; 19 | import { getNextAvailableId } from "../state"; 20 | import { 21 | getWrappingVariableDeclaration, 22 | insertGenericTypeParameter, 23 | insertTypeReference, 24 | } from "../transforms/insertTypes"; 25 | 26 | export async function addATypehole() { 27 | const editor = vscode.window.activeTextEditor; 28 | const document = editor?.document; 29 | if (!editor || !document) { 30 | return; 31 | } 32 | 33 | const fullFile = document.getText(); 34 | const ast = getAST(fullFile); 35 | 36 | const id = getNextAvailableId(); 37 | 38 | await editor.edit((editBuilder) => { 39 | insertTypeholeImport(ast, editBuilder); 40 | 41 | insertRecorderToSelection(id, editor, editBuilder); 42 | }); 43 | 44 | const fileWithImportAndRecorder = document.getText(); 45 | 46 | const updatedAST = getAST(fileWithImportAndRecorder); 47 | 48 | const newlyCreatedTypeHole = last(findTypeholes(updatedAST)); 49 | 50 | const variableDeclaration = 51 | getWrappingVariableDeclaration(newlyCreatedTypeHole); 52 | 53 | const typeName = getPlaceholderTypeName(updatedAST); 54 | 55 | await editor.edit((editBuilder) => { 56 | if (variableDeclaration && !variableDeclaration.type) { 57 | insertTypeToVariableDeclaration( 58 | variableDeclaration, 59 | updatedAST, 60 | editBuilder 61 | ); 62 | } else if (!variableDeclaration) { 63 | insertTypeGenericVariableParameter( 64 | newlyCreatedTypeHole, 65 | typeName, 66 | updatedAST, 67 | editBuilder 68 | ); 69 | } 70 | if (!variableDeclaration || !variableDeclaration.type) { 71 | /* Add a placeholder type */ 72 | insertAPlaceholderType(typeName, editBuilder, newlyCreatedTypeHole); 73 | } 74 | }); 75 | 76 | startRenamingPlaceholderType(typeName, editor, document); 77 | } 78 | 79 | function insertAPlaceholderType( 80 | typeName: string, 81 | editBuilder: vscode.TextEditorEdit, 82 | newTypeHole: ts.CallExpression 83 | ) { 84 | editBuilder.insert( 85 | getEditorRange(getParentOnRootLevel(newTypeHole)).start, 86 | `type ${typeName} = any\n\n` 87 | ); 88 | } 89 | 90 | function insertTypeGenericVariableParameter( 91 | typehole: ts.Node, 92 | typeName: string, 93 | ast: ts.SourceFile, 94 | editBuilder: vscode.TextEditorEdit 95 | ) { 96 | const callExpressionWithGeneric = insertGenericTypeParameter( 97 | typehole, 98 | typeName, 99 | ast 100 | ); 101 | 102 | const start = getNodeStartPosition(typehole); 103 | const end = getNodeEndPosition(typehole); 104 | if (callExpressionWithGeneric) { 105 | editBuilder.replace( 106 | new vscode.Range( 107 | new vscode.Position(start.line, start.character), 108 | new vscode.Position(end.line, end.character) 109 | ), 110 | callExpressionWithGeneric 111 | ); 112 | } 113 | } 114 | 115 | function insertTypeToVariableDeclaration( 116 | variableDeclaration: ts.VariableDeclaration, 117 | ast: ts.SourceFile, 118 | editBuilder: vscode.TextEditorEdit 119 | ) { 120 | const variableDeclationWithNewType = insertTypeReference( 121 | variableDeclaration, 122 | getPlaceholderTypeName(ast), 123 | ast 124 | ); 125 | const start = getNodeStartPosition(variableDeclaration); 126 | const end = getNodeEndPosition(variableDeclaration); 127 | if (variableDeclationWithNewType) { 128 | editBuilder.replace( 129 | new vscode.Range( 130 | new vscode.Position(start.line, start.character), 131 | new vscode.Position(end.line, end.character) 132 | ), 133 | variableDeclationWithNewType 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /packages/extension/src/commands/removeTypeholesFromAllFiles.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { unique } from "../parse/utils"; 3 | import { getAllHoles } from "../state"; 4 | import { removeTypeholesFromFile } from "./removeTypeholesFromCurrentFile"; 5 | 6 | export async function removeTypeholesFromAllFiles() { 7 | const holes = getAllHoles(); 8 | const files = holes.flatMap((h) => h.fileNames).filter(unique); 9 | 10 | for (const file of files) { 11 | let document: null | vscode.TextDocument = null; 12 | try { 13 | document = await vscode.workspace.openTextDocument(vscode.Uri.file(file)); 14 | } catch (error) { 15 | return error("Remove typeholes: Failed to open document", file); 16 | } 17 | 18 | const editor = await vscode.window.showTextDocument(document, 1, false); 19 | removeTypeholesFromFile(editor, document); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/extension/src/commands/removeTypeholesFromCurrentFile.ts: -------------------------------------------------------------------------------- 1 | import { tsquery } from "@phenomnomnominal/tsquery"; 2 | import * as vscode from "vscode"; 3 | import * as ts from "typescript"; 4 | import { getEditorRange } from "../editor/utils"; 5 | import { 6 | findTypeHoleImports, 7 | findTypeholes, 8 | getAST, 9 | printAST, 10 | } from "../parse/module"; 11 | 12 | export async function removeTypeholesFromFile( 13 | editor: vscode.TextEditor, 14 | document: vscode.TextDocument 15 | ) { 16 | const text = document.getText(); 17 | 18 | const ast = getAST(text); 19 | const typeholes = findTypeholes(ast); 20 | 21 | const doesntIncludeConfigureImport = (node: ts.ImportDeclaration) => 22 | tsquery(node, `ImportSpecifier > Identifier[name="configure"]`).length === 23 | 0; 24 | 25 | const importStatements = findTypeHoleImports(ast).filter( 26 | doesntIncludeConfigureImport 27 | ); 28 | 29 | // Cannot be done in just one editBuilder as hopes might overlap each other 30 | // and you'll get Error: Overlapping ranges are not allowed! 31 | 32 | await editor.edit((editBuilder) => { 33 | if (typeholes.length > 0) { 34 | editBuilder.replace( 35 | getEditorRange(typeholes[0]), 36 | printAST(typeholes[0].arguments[0]) 37 | ); 38 | } 39 | 40 | // Remove import statement if it was the last one 41 | if (typeholes.length === 1) { 42 | importStatements.forEach((statement) => 43 | editBuilder.delete(getEditorRange(statement)) 44 | ); 45 | } 46 | }); 47 | 48 | if (typeholes.length > 1) { 49 | await removeTypeholesFromFile(editor, document); 50 | } 51 | } 52 | 53 | export async function removeTypeholesFromCurrentFile() { 54 | const editor = vscode.window.activeTextEditor; 55 | const document = editor?.document; 56 | if (!document || !editor) { 57 | return; 58 | } 59 | return removeTypeholesFromFile(editor, document); 60 | } 61 | -------------------------------------------------------------------------------- /packages/extension/src/config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export type PackageManager = "yarn" | "npm" | undefined; 4 | 5 | export function getConfiguration( 6 | ...params: Parameters 7 | ) { 8 | const configuration = vscode.workspace.getConfiguration(...params); 9 | 10 | return { 11 | extensionPort: configuration.get( 12 | "typehole.runtime.extensionPort" 13 | ) as number, 14 | typeOrInterface: configuration.get("typehole.typeOrInterface") as 15 | | "interface" 16 | | "type", 17 | autoInstall: configuration.get("typehole.runtime.autoInstall") as boolean, 18 | projectPath: configuration.get("typehole.runtime.projectPath") as string, 19 | packageManager: configuration.get( 20 | "typehole.runtime.packageManager" 21 | ) as PackageManager, 22 | update: configuration.update, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/extension/src/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { events, getWarnings, State } from "./state"; 3 | export const diagnosticCollection = 4 | vscode.languages.createDiagnosticCollection("typehole"); 5 | 6 | events.on("change", (newState: State) => { 7 | diagnosticCollection.clear(); 8 | Object.keys(newState.warnings).forEach((file) => { 9 | diagnosticCollection.set( 10 | vscode.Uri.file(file), 11 | getWarnings(file).map( 12 | (range) => 13 | new vscode.Diagnostic( 14 | range, 15 | "This value cannot be automatically typed by Typehole. Either the value is not JSON serializable (function, promise etc.) or it contains a cyclic values i.e. when an object references itself.", 16 | vscode.DiagnosticSeverity.Warning 17 | ) 18 | ) 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/extension/src/editor/utils.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as vscode from "vscode"; 3 | import { getConfiguration } from "../config"; 4 | import { getNodeEndPosition, getNodeStartPosition } from "../parse/module"; 5 | 6 | export const getEditorRange = (node: ts.Node) => { 7 | const start = getNodeStartPosition(node); 8 | const end = getNodeEndPosition(node); 9 | return new vscode.Range( 10 | new vscode.Position(start.line, start.character), 11 | new vscode.Position(end.line, end.character) 12 | ); 13 | }; 14 | 15 | export function getProjectURI() { 16 | if (!vscode.workspace.workspaceFolders) { 17 | return; 18 | } 19 | return vscode.workspace.workspaceFolders[0].uri; 20 | } 21 | export function getProjectPath() { 22 | return getProjectURI()?.path; 23 | } 24 | 25 | export async function getPackageJSONDirectories() { 26 | const include = new vscode.RelativePattern( 27 | getProjectURI()!, 28 | "**/package.json" 29 | ); 30 | 31 | const exclude = new vscode.RelativePattern( 32 | getProjectURI()!, 33 | "**/node_modules/**" 34 | ); 35 | 36 | const files = await vscode.workspace.findFiles(include, exclude); 37 | 38 | // Done like this as findFiles didn't respect the exclude parameter 39 | return files.filter((f) => !f.path.includes("node_modules")); 40 | } 41 | 42 | export async function resolveProjectRoot( 43 | document: vscode.TextDocument, 44 | options: vscode.Uri[] 45 | ) { 46 | const config = getConfiguration("", document.uri); 47 | const answer = await vscode.window.showQuickPick( 48 | options.map((o) => o.path.replace("/package.json", "")), 49 | { 50 | placeHolder: "Where should the runtime package be installed?", 51 | } 52 | ); 53 | 54 | if (answer) { 55 | config.update( 56 | "typehole.runtime.projectPath", 57 | answer, 58 | vscode.ConfigurationTarget.Workspace 59 | ); 60 | return answer; 61 | } 62 | } 63 | 64 | export async function getProjectRoot(document: vscode.TextDocument) { 65 | const config = getConfiguration("", document.uri); 66 | 67 | const packageRoots = await getPackageJSONDirectories(); 68 | 69 | let projectPath = getProjectPath(); 70 | 71 | if (packageRoots.length > 1) { 72 | return ( 73 | config.projectPath || (await resolveProjectRoot(document, packageRoots)) 74 | ); 75 | } 76 | 77 | return projectPath; 78 | } 79 | -------------------------------------------------------------------------------- /packages/extension/src/ensureRuntime.ts: -------------------------------------------------------------------------------- 1 | import { install, setPackageManager, setRootDir } from "lmify"; 2 | import * as vscode from "vscode"; 3 | 4 | import { getConfiguration, PackageManager } from "./config"; 5 | import { getProjectRoot } from "./editor/utils"; 6 | import { error, log } from "./logger"; 7 | 8 | /* 9 | * A bit of a hack as require.resolve doesn't update it's cache 10 | * when a module is installed while the extension is running 11 | */ 12 | 13 | let runtimeWasInstalledWhileExtensionIsRunning = false; 14 | 15 | async function detectPackageManager(): Promise { 16 | const npmLocks = await vscode.workspace.findFiles("package-lock.json"); 17 | const yarnLocks = await vscode.workspace.findFiles("yarn.lock"); 18 | 19 | if (npmLocks.length > 0 && yarnLocks.length === 0) { 20 | return "npm"; 21 | } 22 | 23 | if (yarnLocks.length > 0 && npmLocks.length === 0) { 24 | return "yarn"; 25 | } 26 | } 27 | 28 | async function getPackageManager(document: vscode.TextDocument) { 29 | const config = getConfiguration("", document.uri); 30 | if (config.packageManager) { 31 | return config.packageManager; 32 | } 33 | 34 | let packageManager = await detectPackageManager(); 35 | if (packageManager) { 36 | return packageManager; 37 | } 38 | 39 | packageManager = (await vscode.window.showQuickPick(["npm", "yarn"], { 40 | placeHolder: 41 | "Which package manager should Typehole use to install the runtime package?", 42 | })) as PackageManager; 43 | 44 | if (packageManager) { 45 | config.update( 46 | "typehole.runtime.packageManager", 47 | packageManager, 48 | vscode.ConfigurationTarget.Workspace 49 | ); 50 | return packageManager; 51 | } 52 | } 53 | 54 | function isRuntimeInstalled(projectRoot: string) { 55 | try { 56 | log("Searching for runtime library in", projectRoot); 57 | require.resolve("typehole", { 58 | paths: [projectRoot], 59 | }); 60 | return true; 61 | } catch (error) { 62 | log(error.message); 63 | return false; 64 | } 65 | } 66 | let installing = false; 67 | 68 | export function isInstalling() { 69 | return installing; 70 | } 71 | 72 | export async function ensureRuntime() { 73 | const editor = vscode.window.activeTextEditor; 74 | const document = editor?.document; 75 | 76 | if (!document) { 77 | return; 78 | } 79 | 80 | const config = getConfiguration("", document.uri); 81 | 82 | let packageManager = await getPackageManager(document); 83 | 84 | if (!packageManager) { 85 | return; 86 | } 87 | setPackageManager(packageManager); 88 | 89 | const projectPath = await getProjectRoot(document); 90 | 91 | if (!projectPath) { 92 | return; 93 | } 94 | 95 | const installed = 96 | isRuntimeInstalled(projectPath) || 97 | runtimeWasInstalledWhileExtensionIsRunning; 98 | 99 | if (!installed && config.autoInstall) { 100 | installing = true; 101 | vscode.window.showInformationMessage( 102 | "Typehole: Installing runtime package..." 103 | ); 104 | 105 | log("Detecting package manager from", projectPath); 106 | 107 | try { 108 | (setRootDir as any)(projectPath); 109 | await install(["-D", "typehole", "--no-save"]); 110 | log("Runtime installed"); 111 | installing = false; 112 | runtimeWasInstalledWhileExtensionIsRunning = true; 113 | vscode.window.showInformationMessage( 114 | "Typehole: Runtime package installed" 115 | ); 116 | } catch (err) { 117 | installing = false; 118 | error(err.message); 119 | vscode.window.showErrorMessage( 120 | 'Typehole: Failed to install runtime.\nInstall it manually by running "npm install typehole"' 121 | ); 122 | } 123 | } else if (!installed) { 124 | vscode.window.showErrorMessage(`Typehole: Install the runtime by running 125 | "npm install typehole" or 126 | "yarn add typehole"`); 127 | return; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /packages/extension/src/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { tsquery } from "@phenomnomnominal/tsquery"; 2 | import * as assert from "assert"; 3 | 4 | import { findTypeholes, getAST } from "./parse/module"; 5 | 6 | import { getAllDependencyTypeDeclarations } from "./transforms/insertTypes"; 7 | 8 | test("finds all typewholes from source", () => { 9 | const actual = findTypeholes(` 10 | import { VercelRequest, VercelResponse } from "@vercel/node"; 11 | import axios from "axios"; 12 | import typehole from "typehole"; 13 | 14 | export default async (request: VercelRequest, response: VercelResponse) => { 15 | const xsrf = await getXSRF(); 16 | const res = typehole.t( 17 | ( 18 | await axios.post( 19 | "https://www.etuovi.com/api/v2/announcements/search/listpage", 20 | params 21 | ) 22 | ).data 23 | ); 24 | 25 | return response.status(200).send(res.announcements); 26 | }; 27 | 28 | `); 29 | assert.strictEqual(actual.length, 1); 30 | }); 31 | 32 | test("Finds all dependency type nodes from an AST", () => { 33 | const ast = getAST(`type Something = { 34 | a: B; 35 | }; 36 | 37 | type B = { 38 | moi: C | D; 39 | }; 40 | 41 | type C = 2; 42 | 43 | type D = 3;`); 44 | const actual = getAllDependencyTypeDeclarations( 45 | tsquery.query(ast, ':declaration > Identifier[name="Something"]')[0].parent 46 | ); 47 | 48 | expect(actual.map((n) => n.getText())).toMatchSnapshot(); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { wrapIntoRecorder } from "./transforms/wrapIntoRecorder"; 4 | import { 5 | getAST, 6 | findTypeHoleImports, 7 | getNodeEndPosition, 8 | getTypeHoleImport, 9 | findLastImport, 10 | getNodeStartPosition, 11 | } from "./parse/module"; 12 | import * as ts from "typescript"; 13 | 14 | import { tsquery } from "@phenomnomnominal/tsquery"; 15 | import { 16 | getDescendantAtRange, 17 | lineCharacterPositionInText, 18 | } from "./parse/utils"; 19 | import { 20 | isServerRunning, 21 | startListenerServer, 22 | stopListenerServer, 23 | } from "./listener"; 24 | import { getEditorRange, getProjectURI } from "./editor/utils"; 25 | import { TypeHoler } from "./code-action"; 26 | import { 27 | clearWarnings, 28 | events, 29 | getAllHoles, 30 | getState, 31 | onFileChanged, 32 | onFileDeleted, 33 | State, 34 | } from "./state"; 35 | 36 | import { readFile } from "fs"; 37 | import { log } from "./logger"; 38 | import { addATypehole } from "./commands/addATypehole"; 39 | import { removeTypeholesFromAllFiles } from "./commands/removeTypeholesFromAllFiles"; 40 | import { removeTypeholesFromCurrentFile } from "./commands/removeTypeholesFromCurrentFile"; 41 | import { diagnosticCollection } from "./diagnostics"; 42 | import { ensureRuntime, isInstalling } from "./ensureRuntime"; 43 | import { getConfiguration } from "./config"; 44 | 45 | export const last = (arr: T[]) => arr[arr.length - 1]; 46 | 47 | export function getPlaceholderTypeName(document: ts.SourceFile) { 48 | let n = 0; 49 | 50 | let results = tsquery.query(document, `Identifier[name="AutoDiscovered"]`); 51 | 52 | while (results.length > 0) { 53 | n++; 54 | 55 | results = tsquery.query(document, `Identifier[name="AutoDiscovered${n}"]`); 56 | } 57 | 58 | return "AutoDiscovered" + (n === 0 ? "" : n); 59 | } 60 | 61 | export function startRenamingPlaceholderType( 62 | typeName: string, 63 | editor: vscode.TextEditor, 64 | document: vscode.TextDocument 65 | ) { 66 | const fullFile = document.getText(); 67 | const ast = getAST(fullFile); 68 | 69 | tsquery 70 | .query(ast, `TypeAliasDeclaration > Identifier[name="${typeName}"]`) 71 | .forEach(async (node) => { 72 | const start = getNodeStartPosition(node); 73 | const end = getNodeEndPosition(node); 74 | 75 | editor.selection = new vscode.Selection( 76 | new vscode.Position(start.line, start.character), 77 | new vscode.Position(end.line, end.character) 78 | ); 79 | 80 | await vscode.commands.executeCommand("editor.action.rename"); 81 | }); 82 | } 83 | 84 | export function insertTypeholeImport( 85 | ast: ts.Node, 86 | editBuilder: vscode.TextEditorEdit 87 | ) { 88 | const lastImport = findLastImport(ast); 89 | const position = lastImport 90 | ? getNodeEndPosition(lastImport) 91 | : new vscode.Position(0, 0); 92 | 93 | const existingImports = findTypeHoleImports(ast); 94 | 95 | if (existingImports.length === 0) { 96 | editBuilder.insert( 97 | new vscode.Position(position.line, position.character), 98 | "\n" + getTypeHoleImport() + "\n" 99 | ); 100 | } 101 | } 102 | 103 | export function insertRecorderToSelection( 104 | id: number, 105 | editor: vscode.TextEditor, 106 | editBuilder: vscode.TextEditorEdit 107 | ) { 108 | const fullFile = editor.document.getText(); 109 | const range = editor.selection; 110 | 111 | const startPosition = lineCharacterPositionInText(range.start, fullFile); 112 | const endPosition = lineCharacterPositionInText(range.end, fullFile); 113 | 114 | const selectedNode = getDescendantAtRange(getAST(fullFile), [ 115 | startPosition, 116 | endPosition, 117 | ]); 118 | 119 | const nodeRange = getEditorRange(selectedNode); 120 | 121 | editBuilder.replace(nodeRange, wrapIntoRecorder(id, selectedNode)); 122 | } 123 | 124 | export async function activate(context: vscode.ExtensionContext) { 125 | log("Plugin activated"); 126 | 127 | context.subscriptions.push(diagnosticCollection); 128 | 129 | const typescriptFilesInTheProject = new vscode.RelativePattern( 130 | getProjectURI()!, 131 | "**/*.{tsx,ts}" 132 | ); 133 | 134 | /* 135 | * Start and stop HTTP listener based on the amount of holes 136 | */ 137 | 138 | let previousState = getState(); 139 | events.on("change", async (newState: State) => { 140 | const previousHoles = Object.values(previousState.holes); 141 | const newHoles = Object.values(newState.holes); 142 | const allHolesRemoved = previousHoles.length > 0 && newHoles.length === 0; 143 | 144 | const shouldEnsureRuntime = 145 | previousHoles.length !== newHoles.length && newHoles.length > 0; 146 | 147 | previousState = newState; 148 | 149 | if (allHolesRemoved) { 150 | vscode.window.showInformationMessage("Typehole: Stopping the server"); 151 | await stopListenerServer(); 152 | vscode.window.showInformationMessage("Typehole: Server stopped"); 153 | } 154 | 155 | if (shouldEnsureRuntime && !isInstalling()) { 156 | await ensureRuntime(); 157 | } 158 | 159 | if (newHoles.length > 0 && !isServerRunning()) { 160 | const editor = vscode.window.activeTextEditor; 161 | const document = editor?.document; 162 | const config = getConfiguration("", document); 163 | try { 164 | vscode.window.showInformationMessage("Typehole: Starting server..."); 165 | await startListenerServer(config.extensionPort); 166 | vscode.window.showInformationMessage("Typehole: Server ready"); 167 | } catch (error) { 168 | vscode.window.showErrorMessage( 169 | "Typehole failed to start the HTTP listener: " + error.message 170 | ); 171 | } 172 | } 173 | }); 174 | 175 | /* 176 | * Initialize state 177 | */ 178 | 179 | const existingFiles = await vscode.workspace.findFiles( 180 | typescriptFilesInTheProject, 181 | null, 182 | 50 183 | ); 184 | 185 | await Promise.all(existingFiles.map(fileChanged)); 186 | const holes = getAllHoles(); 187 | log("Found", holes.length.toString(), "holes in the workspace"); 188 | 189 | /* 190 | * Setup file watchers to enable holes in multile files 191 | */ 192 | 193 | const watcher = vscode.workspace.createFileSystemWatcher( 194 | typescriptFilesInTheProject, 195 | false, 196 | false, 197 | false 198 | ); 199 | 200 | vscode.workspace.onDidChangeTextDocument((event) => { 201 | onFileChanged(event.document.uri.path, event.document.getText()); 202 | }); 203 | watcher.onDidChange(fileChanged); 204 | watcher.onDidCreate(fileChanged); 205 | watcher.onDidDelete((uri) => { 206 | onFileDeleted(uri.path); 207 | }); 208 | 209 | context.subscriptions.push( 210 | vscode.languages.registerCodeActionsProvider( 211 | ["typescript", "typescriptreact"], 212 | new TypeHoler() 213 | ) 214 | ); 215 | 216 | vscode.commands.registerCommand("typehole.stop-server", async () => { 217 | stopListenerServer(); 218 | }); 219 | 220 | vscode.commands.registerCommand("typehole.start-server", async () => { 221 | const editor = vscode.window.activeTextEditor; 222 | const document = editor?.document; 223 | const config = getConfiguration("", document); 224 | startListenerServer(config.extensionPort); 225 | }); 226 | vscode.commands.registerCommand( 227 | "typehole.remove-from-current-file", 228 | removeTypeholesFromCurrentFile 229 | ); 230 | vscode.commands.registerCommand( 231 | "typehole.remove-from-all-files", 232 | removeTypeholesFromAllFiles 233 | ); 234 | 235 | vscode.commands.registerCommand("typehole.add-a-typehole", addATypehole); 236 | } 237 | 238 | // this method is called when your extension is deactivated 239 | export function deactivate() { 240 | stopListenerServer(); 241 | } 242 | 243 | function fileChanged(uri: vscode.Uri) { 244 | clearWarnings(uri.path); 245 | return new Promise((resolve) => { 246 | readFile(uri.fsPath, (err, data) => { 247 | if (err) { 248 | return log(err.message); 249 | } 250 | onFileChanged(uri.path, data.toString()); 251 | resolve(); 252 | }); 253 | }); 254 | } 255 | -------------------------------------------------------------------------------- /packages/extension/src/hole.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | 4 | export function getId(node: ts.CallExpression) { 5 | const expression = node.expression as ts.PropertyAccessExpression; 6 | const name = expression.name.getText(); 7 | return name; 8 | } 9 | -------------------------------------------------------------------------------- /packages/extension/src/listener.ts: -------------------------------------------------------------------------------- 1 | import { tsquery } from "@phenomnomnominal/tsquery"; 2 | import f from "fastify"; 3 | 4 | import * as ts from "typescript"; 5 | import * as vscode from "vscode"; 6 | import { getConfiguration } from "./config"; 7 | 8 | import { getEditorRange, getProjectRoot } from "./editor/utils"; 9 | import { error, log } from "./logger"; 10 | import { findTypeholes, getAST, resolveImportPath } from "./parse/module"; 11 | import { addSample, addWarning, getHole, Typehole } from "./state"; 12 | import { 13 | findDeclarationWithName, 14 | getAllDependencyTypeDeclarations, 15 | getTypeAliasForId, 16 | getTypeReferenceNameForId, 17 | } from "./transforms/insertTypes"; 18 | import { samplesToType } from "./transforms/samplesToType"; 19 | 20 | let running = false; 21 | export function isServerRunning() { 22 | return running; 23 | } 24 | let server = createServer(); 25 | 26 | function createServer() { 27 | const fastify = f({ logger: true }); 28 | fastify.register(require("fastify-cors")); 29 | 30 | fastify.post("/type", async (request, reply) => { 31 | vscode.window.showWarningMessage( 32 | "Typehole: You seem to be running an old version of the runtime. Remove 'typehole' package from node_modules and add a new typehole to download the latest version or install it manually." 33 | ); 34 | return reply.code(200).send(); 35 | }); 36 | 37 | fastify.post("/samples", async (request, reply) => { 38 | const body = request.body as any; 39 | log(body.id, "-", "New sample", JSON.stringify(request.body), "received"); 40 | 41 | const editor = vscode.window.activeTextEditor; 42 | const document = editor?.document; 43 | const config = getConfiguration("", document); 44 | 45 | const samples = addSample(body.id, body.sample); 46 | const typeString = samplesToType(samples, { 47 | useTypeAlias: config.typeOrInterface === "type", 48 | }); 49 | 50 | try { 51 | await onTypeExtracted(body.id, typeString); 52 | } catch (err) { 53 | error(err.message); 54 | } 55 | 56 | return reply.code(200).send(); 57 | }); 58 | 59 | fastify.post("/unserializable", async (request, reply) => { 60 | const body = request.body as any; 61 | error("Value in typehole", body.id, "is unserializable"); 62 | onUnserializable(body.id); 63 | return reply.code(200).send(); 64 | }); 65 | return fastify; 66 | } 67 | 68 | export async function startListenerServer(port: number) { 69 | log("Requesting HTTP server start"); 70 | 71 | running = true; 72 | try { 73 | await server.listen(port); 74 | log("HTTP server started"); 75 | } catch (err) { 76 | error("Starting HTTP server failed"); 77 | running = false; 78 | console.error(err); 79 | throw err; 80 | } 81 | } 82 | 83 | export async function stopListenerServer() { 84 | log("Stopping the HTTP server"); 85 | try { 86 | await server.close(); 87 | // Server is recreated as Fastify doesn't support closing and restarting a server 88 | // https://github.com/fastify/fastify/issues/2411 89 | server = createServer(); 90 | running = false; 91 | log("HTTP server server stopped"); 92 | } catch (error) { 93 | running = false; 94 | } 95 | } 96 | 97 | async function onTypeExtracted(id: string, types: string) { 98 | const hole = getHole(id); 99 | 100 | if (!hole) { 101 | error("Hole", id, "was not found. This is not supposed to happen"); 102 | return; 103 | } 104 | 105 | for (const fileName of hole.fileNames) { 106 | await updateTypes(hole, types, fileName); 107 | } 108 | } 109 | 110 | async function updateTypes(hole: Typehole, types: string, fileName: string) { 111 | let document = await vscode.workspace.openTextDocument( 112 | vscode.Uri.file(fileName) 113 | ); 114 | 115 | if (!document) { 116 | error( 117 | "Document", 118 | fileName, 119 | "a typehole was referring to was not found. This is not supposed to happen" 120 | ); 121 | return; 122 | } 123 | 124 | let ast = getAST(document.getText()); 125 | 126 | let typeAliasNode = getTypeAliasForId(hole.id, ast); 127 | 128 | if (!typeAliasNode) { 129 | return; 130 | } 131 | 132 | const typeName = getTypeReferenceNameForId(hole.id, ast)!; 133 | 134 | /* 135 | * Type is imported from another file 136 | */ 137 | const typeIsImportedFromAnotherFile = ts.isImportDeclaration(typeAliasNode); 138 | if (typeIsImportedFromAnotherFile) { 139 | const relativePath = tsquery(typeAliasNode, "StringLiteral")[0] 140 | ?.getText() 141 | // "./types.ts" -> types.ts 142 | .replace(/["']/g, ""); 143 | 144 | const projectRoot = await getProjectRoot(document); 145 | 146 | if (!projectRoot) { 147 | return error("No project root was found when resolving module import"); 148 | } 149 | 150 | const absolutePath = resolveImportPath( 151 | projectRoot, 152 | relativePath, 153 | document.uri.path 154 | ); 155 | 156 | if (!absolutePath) { 157 | return error("TS Compiler couldn't resolve the import path"); 158 | } 159 | 160 | try { 161 | document = await vscode.workspace.openTextDocument( 162 | vscode.Uri.file(absolutePath) 163 | ); 164 | } catch (err) { 165 | return error( 166 | "Failed to open the document the imported type is referring to", 167 | absolutePath, 168 | err.message 169 | ); 170 | } 171 | 172 | ast = getAST(document.getText()); 173 | typeAliasNode = findDeclarationWithName(typeName, ast); 174 | if (!typeAliasNode) { 175 | return; 176 | } 177 | } 178 | 179 | const exported = tsquery(typeAliasNode.parent, "ExportKeyword").length > 0; 180 | const existingDeclarations = getAllDependencyTypeDeclarations( 181 | typeAliasNode.parent 182 | ); 183 | 184 | const typesToBeInserted = 185 | (exported ? "export " : "") + 186 | types.replace("TypeholeRoot", typeName).trim(); 187 | 188 | const workEdits = new vscode.WorkspaceEdit(); 189 | 190 | existingDeclarations.forEach((node) => { 191 | const range = getEditorRange(node); 192 | workEdits.delete(document?.uri!, range); 193 | }); 194 | 195 | workEdits.insert( 196 | document.uri, 197 | getEditorRange(typeAliasNode!.parent).start, 198 | typesToBeInserted 199 | ); 200 | 201 | await vscode.workspace.applyEdit(workEdits); 202 | 203 | try { 204 | const workEdits = new vscode.WorkspaceEdit(); 205 | const edits = await vscode.commands.executeCommand( 206 | "vscode.executeFormatDocumentProvider", 207 | document.uri 208 | ); 209 | 210 | if (edits) { 211 | workEdits.set(document.uri, edits); 212 | await vscode.workspace.applyEdit(workEdits); 213 | } 214 | } catch (err) { 215 | error("Formatting the document failed", err.message); 216 | } 217 | } 218 | 219 | async function onUnserializable(id: string) { 220 | const editor = vscode.window.activeTextEditor; 221 | const document = editor?.document; 222 | if (!editor || !document) { 223 | return; 224 | } 225 | 226 | const ast = getAST(editor.document.getText()); 227 | const holes = findTypeholes(ast); 228 | 229 | const hole = holes.find( 230 | (h) => 231 | ts.isPropertyAccessExpression(h.expression) && 232 | h.expression.name.getText() === id 233 | ); 234 | 235 | if (hole) { 236 | const range = getEditorRange(hole); 237 | addWarning(document.uri.path, range); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /packages/extension/src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const logger = vscode.window.createOutputChannel("Typehole"); 4 | 5 | export const log = (...messages: string[]) => 6 | logger.appendLine(["Info:"].concat(messages).join(" ")); 7 | 8 | export const warn = (...messages: string[]) => 9 | logger.appendLine(["Warn:"].concat(messages).join(" ")); 10 | 11 | export const error = (...messages: string[]) => 12 | logger.appendLine(["Error:"].concat(messages).join(" ")); 13 | -------------------------------------------------------------------------------- /packages/extension/src/parse/expression.test.ts: -------------------------------------------------------------------------------- 1 | import { tsquery } from "@phenomnomnominal/tsquery"; 2 | import * as assert from "assert"; 3 | 4 | import { isValidSelection } from "./expression"; 5 | import { getAST } from "./module"; 6 | 7 | const toNode = (str: string) => getAST(str).getChildAt(0).getChildAt(0); 8 | 9 | test("finds selected expression", () => { 10 | const actual = isValidSelection( 11 | toNode(` 12 | tsquery.query( 13 | ast, 14 | "InterfaceDeclaration > Identifier[name='AutoDiscover']" 15 | ) 16 | `) 17 | ); 18 | 19 | assert.deepStrictEqual(actual, true); 20 | }); 21 | 22 | test("finds selected expression", () => { 23 | const actual = isValidSelection( 24 | toNode(` 25 | (await axios.post( 26 | "https://www.etuovi.com/api/v2/announcements/search/listpage", 27 | params 28 | ) 29 | ).data 30 | `) 31 | ); 32 | 33 | assert.deepStrictEqual(actual, true); 34 | }); 35 | 36 | test("returns null on non-expression selection", () => { 37 | const actual = isValidSelection( 38 | toNode(` 39 | if (!siblings.some((s) => markerStarts.includes(s))) { 40 | return ts.visitEachChild(node, visitor, ctx); 41 | } 42 | `) 43 | ); 44 | assert.strictEqual(actual, false); 45 | }); 46 | 47 | test("it's ok to select specific values", () => { 48 | assert.strictEqual(isValidSelection(toNode(`undefined`)), true); 49 | assert.strictEqual(isValidSelection(toNode(`1`)), true); 50 | assert.strictEqual(isValidSelection(toNode(`true`)), true); 51 | assert.strictEqual( 52 | isValidSelection( 53 | tsquery.query(toNode(`const a = {a: 3}`), "ObjectLiteralExpression")[0] 54 | ), 55 | true 56 | ); 57 | }); 58 | 59 | test("returns null on non-expression selection", () => { 60 | const actual = isValidSelection( 61 | toNode(` 62 | .some((s) => markerStarts.includes(s))) { 63 | return ts.visitEachChild(node, visitor, ctx); 64 | } 65 | `) 66 | ); 67 | assert.strictEqual(actual, false); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/extension/src/parse/expression.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | export function isValidSelection(selectedNode: ts.Node) { 4 | const kind = ts.SyntaxKind[selectedNode.kind]; 5 | 6 | return ( 7 | kind.includes("Expression") || 8 | kind.includes("Literal") || 9 | kind.includes("Identifier") || 10 | kind.includes("Keyword") 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/extension/src/parse/module.test.ts: -------------------------------------------------------------------------------- 1 | import { findTypeholes, getAST } from "./module"; 2 | 3 | test("finds correct amount of typeholes", () => { 4 | expect(findTypeholes(getAST(file)).length).toEqual(2); 5 | }); 6 | 7 | test("returns holes in creation order", () => { 8 | expect( 9 | findTypeholes(getAST(file)).map((n: any) => n.expression.name.getText()) 10 | ).toEqual(["t", "t1"]); 11 | }); 12 | 13 | const file = `import logo from "./logo.svg"; 14 | import "./App.css"; 15 | import typehole from "typehole"; 16 | 17 | function App() { 18 | return ( 19 |
20 |
21 | logo 22 |

23 | Edit src/App.tsx and save to {typehole.t("reload")} 24 |

25 | 31 | Learn React 32 | 33 |
34 |
35 | ); 36 | } 37 | 38 | export default App;`; 39 | -------------------------------------------------------------------------------- /packages/extension/src/parse/module.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { tsquery } from "@phenomnomnominal/tsquery"; 3 | 4 | export function findTypeHoleImports(ast: ts.Node) { 5 | return tsquery 6 | .query(ast, "ImportDeclaration > StringLiteral[text='typehole']") 7 | .map((s) => s.parent as ts.ImportDeclaration); 8 | } 9 | 10 | export function resolveImportPath( 11 | projectRoot: string, 12 | moduleName: string, 13 | containingFile: string 14 | ) { 15 | const configFileName = ts.findConfigFile( 16 | projectRoot, 17 | ts.sys.fileExists, 18 | "tsconfig.json" 19 | ); 20 | 21 | if (!configFileName) { 22 | return null; 23 | } 24 | 25 | const configFile = ts.readConfigFile(configFileName, ts.sys.readFile); 26 | 27 | const compilerOptions = ts.parseJsonConfigFileContent( 28 | configFile.config, 29 | ts.sys, 30 | "./", 31 | undefined, 32 | configFileName 33 | ); 34 | 35 | function fileExists(fileName: string): boolean { 36 | return ts.sys.fileExists(fileName); 37 | } 38 | 39 | function readFile(fileName: string): string | undefined { 40 | return ts.sys.readFile(fileName); 41 | } 42 | 43 | const result = ts.resolveModuleName( 44 | moduleName, 45 | containingFile, 46 | compilerOptions.options, 47 | { 48 | fileExists, 49 | readFile, 50 | } 51 | ); 52 | 53 | return result.resolvedModule!.resolvedFileName; 54 | } 55 | 56 | export function findLastImport(ast: ts.Node) { 57 | const imports = tsquery.query(ast, "ImportDeclaration"); 58 | return imports[imports.length - 1]; 59 | } 60 | 61 | export function getNodeEndPosition(node: ts.Node) { 62 | return node.getSourceFile().getLineAndCharacterOfPosition(node.getEnd()); 63 | } 64 | export function getNodeStartPosition(node: ts.Node) { 65 | return node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()); 66 | } 67 | 68 | export function getTypeHoleImport() { 69 | const clause = ts.factory.createImportClause( 70 | false, 71 | ts.factory.createIdentifier("typehole"), 72 | undefined 73 | ); 74 | return printAST( 75 | ts.factory.createImportDeclaration( 76 | undefined, 77 | undefined, 78 | clause, 79 | ts.factory.createStringLiteral("typehole") 80 | ) 81 | ); 82 | } 83 | 84 | export function printAST(ast: ts.Node, sourceFile?: ts.SourceFile) { 85 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 86 | 87 | return printer.printNode( 88 | ts.EmitHint.Unspecified, 89 | ast, 90 | sourceFile || ast.getSourceFile() 91 | ); 92 | } 93 | 94 | export function findTypeholes(ast: ts.Node | string): ts.CallExpression[] { 95 | const holes = tsquery.query( 96 | ast, 97 | `PropertyAccessExpression > Identifier[name="typehole"]` 98 | ); 99 | 100 | return holes 101 | .map((n) => n.parent.parent) 102 | .filter(ts.isCallExpression) 103 | .sort((a, b) => { 104 | const keyA = (a.expression as ts.PropertyAccessExpression).name.getText(); 105 | const keyB = (b.expression as ts.PropertyAccessExpression).name.getText(); 106 | return keyA.localeCompare(keyB); 107 | }); 108 | } 109 | 110 | export function getAST(source: string) { 111 | return tsquery.ast(source, "file.ts", ts.ScriptKind.TSX); 112 | } 113 | 114 | export function getParentOnRootLevel(node: ts.Node): ts.Node { 115 | if (ts.isSourceFile(node.parent)) { 116 | return node; 117 | } 118 | return getParentOnRootLevel(node.parent); 119 | } 120 | export function someParentIs( 121 | node: ts.Node, 122 | test: (node: ts.Node) => boolean 123 | ): boolean { 124 | if (!node.parent) { 125 | return false; 126 | } 127 | if (test(node.parent)) { 128 | return true; 129 | } 130 | return someParentIs(node.parent, test); 131 | } 132 | 133 | export function getParentWithType( 134 | node: ts.Node, 135 | kind: ts.SyntaxKind 136 | ): T | null { 137 | if (!node.parent) { 138 | return null; 139 | } 140 | if (node.kind === kind) { 141 | return node as unknown as T; 142 | } 143 | return getParentWithType(node.parent, kind); 144 | } 145 | -------------------------------------------------------------------------------- /packages/extension/src/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | function getStartSafe(node: ts.Node, sourceFile: ts.SourceFile) { 4 | // workaround for compiler api bug with getStart(sourceFile, true) (see PR #35029 in typescript repo) 5 | const jsDocs = (node as any).jsDoc as ts.Node[] | undefined; 6 | if (jsDocs && jsDocs.length > 0) { 7 | return jsDocs[0].getStart(sourceFile); 8 | } 9 | return node.getStart(sourceFile); 10 | } 11 | 12 | function allChildren(node: ts.Node): ts.Node[] { 13 | return node.getChildren(); 14 | } 15 | 16 | export function getDescendantAtRange( 17 | sourceFile: ts.SourceFile, 18 | range: [number, number] 19 | ) { 20 | const syntaxKinds = ts.SyntaxKind; 21 | 22 | let bestMatch: { node: ts.Node; start: number } = { 23 | node: sourceFile, 24 | start: sourceFile.getStart(sourceFile), 25 | }; 26 | 27 | searchDescendants(sourceFile); 28 | 29 | return bestMatch.node; 30 | 31 | function searchDescendants(node: ts.Node) { 32 | const children = allChildren(node); 33 | 34 | for (const child of children) { 35 | if (child.kind !== syntaxKinds.SyntaxList) { 36 | if (isBeforeRange(child.end)) { 37 | continue; 38 | } 39 | 40 | const childStart = getStartSafe(child, sourceFile); 41 | 42 | if (isAfterRange(childStart)) { 43 | return; 44 | } 45 | 46 | const isEndOfFileToken = child.kind === syntaxKinds.EndOfFileToken; 47 | if (isEndOfFileToken) { 48 | return; 49 | } 50 | const hasSameStart = 51 | bestMatch.start === childStart && range[0] === childStart; 52 | 53 | if ( 54 | !hasSameStart && 55 | Math.abs(range[0] - bestMatch.start) > Math.abs(range[0] - childStart) 56 | ) { 57 | bestMatch = { node: child, start: childStart }; 58 | } 59 | } 60 | 61 | searchDescendants(child); 62 | } 63 | } 64 | 65 | function isBeforeRange(pos: number) { 66 | return pos < range[0]; 67 | } 68 | 69 | function isAfterRange(nodeEnd: number) { 70 | return nodeEnd >= range[0] && nodeEnd > range[1]; 71 | } 72 | } 73 | 74 | export function lineCharacterPositionInText( 75 | lineChar: ts.LineAndCharacter, 76 | text: string 77 | ) { 78 | const rows = text.split("\n"); 79 | 80 | const allLines = rows.slice(0, lineChar.line + 1); 81 | allLines[allLines.length - 1] = allLines[allLines.length - 1].substr( 82 | 0, 83 | lineChar.character 84 | ); 85 | 86 | return allLines.join("\n").length; 87 | } 88 | 89 | export function unique(value: T, index: number, self: T[]) { 90 | return self.indexOf(value) === index; 91 | } 92 | 93 | export function omit(original: T, key: keyof T) { 94 | const { [key]: value, ...withoutKey } = original; 95 | return withoutKey; 96 | } 97 | -------------------------------------------------------------------------------- /packages/extension/src/state.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import * as vscode from "vscode"; 3 | import { getId } from "./hole"; 4 | import { log } from "./logger"; 5 | import { findTypeholes, getAST } from "./parse/module"; 6 | import { omit, unique } from "./parse/utils"; 7 | 8 | export const events = new EventEmitter(); 9 | 10 | export type Typehole = { id: string; fileNames: string[] }; 11 | 12 | let state = { 13 | nextUniqueId: 0, 14 | warnings: {} as Record, 15 | holes: {} as Record, 16 | samples: {} as Record, 17 | }; 18 | 19 | export type State = typeof state; 20 | 21 | export function getNextAvailableId() { 22 | return state.nextUniqueId; 23 | } 24 | 25 | export function clearWarnings(fileName: string) { 26 | const state = getState(); 27 | setState({ ...state, warnings: { ...state.warnings, [fileName]: [] } }); 28 | } 29 | 30 | export function getWarnings(fileName: string) { 31 | const state = getState(); 32 | return state.warnings[fileName] || []; 33 | } 34 | 35 | export function addWarning(fileName: string, range: vscode.Range) { 36 | const state = getState(); 37 | const alreadyExists = getWarnings(fileName).some( 38 | (w) => w.start.isEqual(range.start) && w.end.isEqual(range.end) 39 | ); 40 | if (alreadyExists) { 41 | return; 42 | } 43 | 44 | setState({ 45 | ...state, 46 | warnings: { 47 | ...state.warnings, 48 | [fileName]: getWarnings(fileName).concat(range), 49 | }, 50 | }); 51 | } 52 | 53 | export function getSamples(id: string) { 54 | return getState().samples[id] || []; 55 | } 56 | 57 | export function addSample(id: string, sample: any) { 58 | const currentState = getState(); 59 | const existing = getSamples(id); 60 | 61 | const newSamples = [sample].concat(existing); 62 | 63 | setState({ 64 | ...currentState, 65 | samples: { 66 | ...currentState.samples, 67 | [id]: newSamples, 68 | }, 69 | }); 70 | return newSamples; 71 | } 72 | 73 | function clearSamples(id: string, currentState: typeof state) { 74 | return { 75 | ...currentState, 76 | samples: { 77 | ...currentState.samples, 78 | [id]: [], 79 | }, 80 | }; 81 | } 82 | 83 | function createTypehole(id: string, fileName: string) { 84 | const existingHole = getHole(id); 85 | const hole = existingHole 86 | ? { id, fileNames: existingHole.fileNames.concat(fileName).filter(unique) } 87 | : { id, fileNames: [fileName] }; 88 | const currentState = getState(); 89 | setState({ 90 | ...currentState, 91 | nextUniqueId: currentState.nextUniqueId + 1, 92 | holes: { ...currentState.holes, [id]: hole }, 93 | }); 94 | } 95 | 96 | function removeTypeholeFromFile(id: string, fileName: string) { 97 | const currentState = getState(); 98 | 99 | const hole = getHole(id); 100 | if (!hole) { 101 | return; 102 | } 103 | const fileFilesWithoutFile = hole?.fileNames.filter( 104 | (file) => file !== fileName 105 | ); 106 | const wasOnlyFileWithTypehole = fileFilesWithoutFile.length === 0; 107 | 108 | if (wasOnlyFileWithTypehole) { 109 | const newHoles = omit(currentState.holes, id); 110 | setState( 111 | clearSamples(id, { 112 | ...currentState, 113 | holes: newHoles, 114 | }) 115 | ); 116 | } else { 117 | const holeWithoutFile = { ...hole, fileNames: fileFilesWithoutFile }; 118 | setState({ 119 | ...currentState, 120 | holes: { ...currentState.holes, [id]: holeWithoutFile }, 121 | }); 122 | } 123 | } 124 | 125 | function setState(newState: typeof state): void { 126 | state = newState; 127 | 128 | events.emit("change", newState); 129 | } 130 | 131 | export function getState() { 132 | return state; 133 | } 134 | 135 | export function getAllHoles() { 136 | return Object.values(getState().holes); 137 | } 138 | 139 | export function onFileDeleted(fileName: string) { 140 | getAllHoles() 141 | .filter((hole) => hole.fileNames.includes(fileName)) 142 | .forEach((h) => removeTypeholeFromFile(h.id, fileName)); 143 | } 144 | 145 | export function onFileChanged(fileName: string, content: string) { 146 | const knownHolesInThisFile = getAllHoles().filter((hole) => 147 | hole.fileNames.includes(fileName) 148 | ); 149 | const knownIds = knownHolesInThisFile.map(({ id }) => id); 150 | 151 | const ast = getAST(content); 152 | const holesInDocument = findTypeholes(ast).map(getId); 153 | 154 | // Update state to reflect current holes in the document 155 | holesInDocument.forEach((holeId) => { 156 | const newHoleWasAdded = !knownIds.includes(holeId); 157 | if (newHoleWasAdded) { 158 | log("Found a new typehole from", fileName); 159 | createTypehole(holeId, fileName); 160 | } 161 | }); 162 | knownIds.forEach((holeId) => { 163 | const holeHasBeenRemoved = !holesInDocument.includes(holeId); 164 | if (holeHasBeenRemoved) { 165 | removeTypeholeFromFile(holeId, fileName); 166 | } 167 | }); 168 | } 169 | 170 | export function getHole(id: string): Typehole | undefined { 171 | return state.holes[id]; 172 | } 173 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/__snapshots__/wrapIntoRecorder.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`wraps expressions into recorder call 1`] = `"typehole.t(tsquery.query(ast, \\"InterfaceDeclaration > Identifier[name='AutoDiscover']\\"))"`; 4 | 5 | exports[`wraps function declaration into a recorder 1`] = ` 6 | "typehole.t(function App() { 7 | return (
8 |
9 | \\"logo\\"/ 10 |

11 | Edit src/App.tsx and save to{\\" \\"} 12 |

13 | 14 | Learn React 15 | 16 |
17 |
); 18 | })" 19 | `; 20 | 21 | exports[`wraps literals into recorder calls 1`] = `"typehole.t(1 + 3 + 4)"`; 22 | 23 | exports[`wraps literals into recorder calls 2`] = `"typehole.t(\\"moro\\" + \\"moro\\")"`; 24 | 25 | exports[`wraps literals into recorder calls 3`] = `"typehole.t([\\"moro\\", \\"moro\\"])"`; 26 | 27 | exports[`wraps literals into recorder calls 4`] = `"typehole.t([() => 3, \\"moro\\"])"`; 28 | 29 | exports[`wraps objects with inner typeholes 1`] = `"typehole.t({ bar: typehole.t1(234) })"`; 30 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/insertTypes/index.test.ts: -------------------------------------------------------------------------------- 1 | import { tsquery } from "@phenomnomnominal/tsquery"; 2 | import { getAST } from "../../parse/module"; 3 | import * as ts from "typescript"; 4 | import { 5 | findAllDependencyTypeDeclarations, 6 | getAllDependencyTypeDeclarations, 7 | getTypeAliasForId, 8 | } from "./index"; 9 | 10 | test("finds all dependency type declarations from an ast when given one interface", () => { 11 | const ast = getAST(file); 12 | const typeAliasNode = getTypeAliasForId("t", ast)!; 13 | 14 | expect( 15 | getAllDependencyTypeDeclarations(typeAliasNode.parent).map((n) => 16 | n.name.getText() 17 | ) 18 | ).toEqual(["Reddit", "ArrayItemA", "ArrayItemB", "IData", "IChildrenItem"]); 19 | }); 20 | 21 | const file = ` 22 | import React, { useEffect } from "react"; 23 | import logo from "./logo.svg"; 24 | import "./App.css"; 25 | import typehole from "typehole"; 26 | 27 | interface Reddit { 28 | kind: string; 29 | array: ArrayItemA | ArrayItemB; 30 | data: IData; 31 | } 32 | interface IData { 33 | modhash?: string; 34 | dist?: number; 35 | children?: IChildrenItem[]; 36 | } 37 | interface IChildrenItem { 38 | kind: string; 39 | data: IData; 40 | } 41 | 42 | type ArrayItemA = number 43 | type ArrayItemB = {data: IData} 44 | 45 | type Numberz = number; 46 | 47 | function App() { 48 | useEffect(() => { 49 | async function fetchVideos() { 50 | const res = await fetch("https://www.reddit.com/r/videos.json"); 51 | 52 | const data: Reddit = typehole.t(await res.json()); 53 | 54 | const a: Numberz = typehole.t1(1 + 1); 55 | } 56 | fetchVideos(); 57 | }, []); 58 | 59 | return ( 60 |
61 | ); 62 | } 63 | 64 | `; 65 | 66 | test("finds all dependency type declarations from an ast when given one interface", () => { 67 | const ast = getAST(` 68 | type Root = {a: (ArrayItemA | ArrayItemB)[]} 69 | type ArrayItemA = number 70 | type ArrayItemB = {data: boolean} 71 | `); 72 | const node = tsquery.query(ast, 'Identifier[name="Root"]')[0]; 73 | expect( 74 | findAllDependencyTypeDeclarations(node.parent).map((n) => n.name.getText()) 75 | ).toEqual(["Root", "ArrayItemA", "ArrayItemB"]); 76 | }); 77 | 78 | test("finds all dependency type declarations from an ast when given one interface", () => { 79 | const ast = getAST(` 80 | type Root = {a: Array} 81 | type ArrayItemA = number 82 | type ArrayItemB = {data: boolean} 83 | `); 84 | const node = tsquery.query(ast, 'Identifier[name="Root"]')[0]; 85 | expect( 86 | findAllDependencyTypeDeclarations(node.parent).map((n) => n.name.getText()) 87 | ).toEqual(["Root", "ArrayItemA", "ArrayItemB"]); 88 | }); 89 | 90 | test("finds all dependency type declarations from an ast when there are array types in union", () => { 91 | const ast = getAST(` 92 | type AutoDiscovered = IRootObjectItem[] | (string | boolean | number)[]; 93 | interface IRootObjectItem { 94 | a?: number; 95 | }`); 96 | 97 | const node = tsquery.query(ast, 'Identifier[name="AutoDiscovered"]')[0]; 98 | expect( 99 | findAllDependencyTypeDeclarations(node.parent).map((n) => n.name.getText()) 100 | ).toEqual(["AutoDiscovered", "IRootObjectItem"]); 101 | }); 102 | 103 | test("finds dependent types from a ParenthesizedType", () => { 104 | const ast = getAST(`export type Foo = (TypeholeRootWrapper | number); 105 | interface TypeholeRootWrapper { 106 | a: number; 107 | } 108 | `); 109 | 110 | const node = tsquery.query(ast, 'Identifier[name="Foo"]')[0]; 111 | 112 | expect( 113 | findAllDependencyTypeDeclarations(node.parent).map((n) => n.name.getText()) 114 | ).toEqual(["Foo", "TypeholeRootWrapper"]); 115 | }); 116 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/insertTypes/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { tsquery } from "@phenomnomnominal/tsquery"; 3 | import { findTypeholes, getParentWithType, printAST } from "../../parse/module"; 4 | import { unique } from "../../parse/utils"; 5 | 6 | function findDeclarationInImportedDeclarations( 7 | name: string, 8 | ast: ts.Node 9 | ): ts.ImportDeclaration | null { 10 | return tsquery 11 | .query(ast, `ImportSpecifier > Identifier[name="${name}"]`) 12 | .concat(tsquery.query(ast, `ImportClause > Identifier[name="${name}"]`)) 13 | .map((node) => 14 | getParentWithType( 15 | node, 16 | ts.SyntaxKind.ImportDeclaration 17 | ) 18 | )[0]; 19 | } 20 | 21 | function findDeclarationsWithName(name: string, ast: ts.Node) { 22 | const res = tsquery.query( 23 | ast, 24 | `:declaration > Identifier[name="${name}"]` 25 | ); 26 | 27 | return res; 28 | } 29 | 30 | export function findDeclarationWithName( 31 | name: string, 32 | ast: ts.Node 33 | ): 34 | | ts.TypeAliasDeclaration 35 | | ts.InterfaceDeclaration 36 | | ts.ImportDeclaration 37 | | null { 38 | const results = findDeclarationsWithName(name, ast); 39 | if (results.length === 0) { 40 | const importStatement = findDeclarationInImportedDeclarations(name, ast); 41 | if (!importStatement) { 42 | return null; 43 | } 44 | 45 | return importStatement; 46 | } 47 | return results[0]; 48 | } 49 | export function getAllDependencyTypeDeclarations( 50 | node: ts.Node 51 | ): Array { 52 | return findAllDependencyTypeDeclarations(node).filter(unique); 53 | } 54 | 55 | export function findAllDependencyTypeDeclarations( 56 | node: ts.Node, 57 | found: ts.Node[] = [] 58 | ): Array { 59 | // To prevent recursion in circular types 60 | if (found.includes(node)) { 61 | return []; 62 | } 63 | 64 | if (ts.isTypeAliasDeclaration(node)) { 65 | if (ts.isTypeLiteralNode(node.type)) { 66 | return [ 67 | node, 68 | ...node.type.members.flatMap((m: any) => 69 | findAllDependencyTypeDeclarations(m.type, [...found, node]) 70 | ), 71 | ]; 72 | } else { 73 | return [ 74 | node, 75 | ...findAllDependencyTypeDeclarations(node.type, [...found, node]), 76 | ]; 77 | } 78 | } 79 | 80 | // interface Bar { a: Foo } 81 | if (ts.isInterfaceDeclaration(node)) { 82 | return [ 83 | node, 84 | ...node.members.flatMap((m: any) => 85 | findAllDependencyTypeDeclarations(m.type, [...found, node]) 86 | ), 87 | ]; 88 | } 89 | 90 | // { a: Foo } 91 | if (ts.isTypeLiteralNode(node)) { 92 | return node.members.flatMap((m: any) => 93 | findAllDependencyTypeDeclarations(m.type, [...found]) 94 | ); 95 | } 96 | 97 | // Foo, Array 98 | if (ts.isTypeReferenceNode(node)) { 99 | // Array 100 | const isArrayWrappedType = node.typeArguments !== undefined; 101 | if (isArrayWrappedType) { 102 | return node.typeArguments!.flatMap((arg) => 103 | findAllDependencyTypeDeclarations(arg, [...found]) 104 | ); 105 | } 106 | 107 | const declarations = findDeclarationsWithName( 108 | node.typeName.getText(), 109 | node.getSourceFile() 110 | ); 111 | 112 | return [ 113 | ...declarations.flatMap((n) => 114 | findAllDependencyTypeDeclarations(n.parent, [...found]) 115 | ), 116 | ]; 117 | } 118 | if (ts.isArrayTypeNode(node)) { 119 | if (ts.isTypeReferenceNode(node.elementType)) { 120 | return findAllDependencyTypeDeclarations(node.elementType, [...found]); 121 | } 122 | // (A | B)[] 123 | if (ts.isParenthesizedTypeNode(node.elementType)) { 124 | return findAllDependencyTypeDeclarations(node.elementType.type, [ 125 | ...found, 126 | ]); 127 | } 128 | } 129 | 130 | // IRootObjectItem[] | (string | boolean | number)[]; 131 | if (ts.isUnionTypeNode(node)) { 132 | return node.types.flatMap((t) => { 133 | const declarations = findDeclarationsWithName( 134 | ts.isArrayTypeNode(t) ? t.elementType.getText() : t.getText(), 135 | t.getSourceFile() 136 | ); 137 | 138 | return [ 139 | ...declarations.flatMap((n) => 140 | findAllDependencyTypeDeclarations(n.parent, [...found]) 141 | ), 142 | ]; 143 | }); 144 | } 145 | // (TypeholeRootWrapper | number) 146 | if (ts.isParenthesizedTypeNode(node)) { 147 | return findAllDependencyTypeDeclarations(node.type, [...found]); 148 | } 149 | if ( 150 | ts.isArrayTypeNode(node) && 151 | ts.isParenthesizedTypeNode(node.elementType) 152 | ) { 153 | if (ts.isUnionTypeNode(node.elementType.type)) { 154 | return node.elementType.type.types.flatMap((t) => 155 | findDeclarationsWithName(t.getText(), t.getSourceFile()) 156 | ); 157 | } 158 | } 159 | return []; 160 | } 161 | 162 | export function getTypeReferenceNameForId( 163 | id: string, 164 | ast: ts.Node 165 | ): string | null { 166 | const holes = findTypeholes(ast); 167 | 168 | const hole = holes.find( 169 | (h) => 170 | ts.isPropertyAccessExpression(h.expression) && 171 | h.expression.name.getText() === id 172 | ); 173 | 174 | if (!hole) { 175 | return null; 176 | } 177 | 178 | const holeHasTypeVariable = 179 | ts.isCallExpression(hole) && 180 | hole.typeArguments && 181 | hole.typeArguments.length > 0; 182 | 183 | if (holeHasTypeVariable) { 184 | return hole.typeArguments![0].getText(); 185 | } 186 | const variableDeclaration = getWrappingVariableDeclaration(hole); 187 | 188 | const holeIsValueInVariableDeclaration = 189 | variableDeclaration && 190 | ts.isVariableDeclaration(variableDeclaration) && 191 | variableDeclaration.type; 192 | 193 | if (holeIsValueInVariableDeclaration) { 194 | const typeReference = ( 195 | variableDeclaration as ts.VariableDeclaration 196 | ).type!.getText(); 197 | return typeReference; 198 | } 199 | return null; 200 | } 201 | export function getTypeAliasForId( 202 | id: string, 203 | ast: ts.Node 204 | ): 205 | | ts.ImportDeclaration 206 | | ts.TypeAliasDeclaration 207 | | ts.InterfaceDeclaration 208 | | null { 209 | const name = getTypeReferenceNameForId(id, ast); 210 | if (!name) { 211 | return null; 212 | } 213 | 214 | return findDeclarationWithName(name, ast); 215 | } 216 | 217 | export function getWrappingVariableDeclaration( 218 | node: ts.Node 219 | ): ts.VariableDeclaration | null { 220 | if (ts.isVariableDeclaration(node)) { 221 | return node; 222 | } 223 | 224 | if (ts.isSourceFile(node.parent)) { 225 | return null; 226 | } 227 | 228 | if (ts.isArrayLiteralExpression(node.parent)) { 229 | return null; 230 | } 231 | if (ts.isObjectLiteralExpression(node.parent)) { 232 | return null; 233 | } 234 | 235 | if (node.parent) { 236 | return getWrappingVariableDeclaration(node.parent); 237 | } 238 | return null; 239 | } 240 | 241 | export function insertTypeReference( 242 | node: ts.Node, 243 | typeId: string, 244 | sourceFile: ts.SourceFile 245 | ) { 246 | if (ts.isVariableDeclaration(node)) { 247 | const newVariableDeclaration = ts.factory.createVariableDeclaration( 248 | node.name, 249 | node.exclamationToken, 250 | ts.factory.createTypeReferenceNode(typeId), 251 | node.initializer 252 | ); 253 | 254 | return printAST(newVariableDeclaration, sourceFile); 255 | } 256 | return null; 257 | } 258 | export function insertGenericTypeParameter( 259 | node: ts.Node, 260 | typeId: string, 261 | sourceFile: ts.SourceFile 262 | ) { 263 | if (ts.isCallExpression(node)) { 264 | const newCallExpression = ts.factory.createCallExpression( 265 | node.expression, 266 | [ 267 | ts.factory.createTypeReferenceNode( 268 | ts.factory.createIdentifier(typeId), 269 | undefined 270 | ), 271 | ], 272 | node.arguments 273 | ); 274 | 275 | return printAST(newCallExpression, sourceFile); 276 | } 277 | return null; 278 | } 279 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/samplesToType/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generates types and interfaces from samples 1`] = ` 4 | "interface TypeholeRoot { 5 | kind: string; 6 | data: Data2; 7 | } 8 | interface Data2 { 9 | modhash: string; 10 | dist: number; 11 | children: Child[]; 12 | after: string; 13 | before: null; 14 | } 15 | interface Child { 16 | kind: string; 17 | data: Data; 18 | } 19 | interface Data { 20 | approved_at_utc: null; 21 | subreddit: string; 22 | selftext: string; 23 | author_fullname: string; 24 | saved: boolean; 25 | mod_reason_title: null; 26 | gilded: number; 27 | clicked: boolean; 28 | title: string; 29 | link_flair_richtext: any[]; 30 | subreddit_name_prefixed: string; 31 | hidden: boolean; 32 | pwls: number; 33 | link_flair_css_class: null | string; 34 | downs: number; 35 | thumbnail_height: number; 36 | top_awarded_type: null; 37 | hide_score: boolean; 38 | name: string; 39 | quarantine: boolean; 40 | link_flair_text_color: string; 41 | upvote_ratio: number; 42 | author_flair_background_color: null; 43 | subreddit_type: string; 44 | ups: number; 45 | total_awards_received: number; 46 | media_embed: Mediaembed; 47 | thumbnail_width: number; 48 | author_flair_template_id: null; 49 | is_original_content: boolean; 50 | user_reports: any[]; 51 | secure_media: Securemedia; 52 | is_reddit_media_domain: boolean; 53 | is_meta: boolean; 54 | category: null; 55 | secure_media_embed: Securemediaembed; 56 | link_flair_text: null | string; 57 | can_mod_post: boolean; 58 | score: number; 59 | approved_by: null; 60 | author_premium: boolean; 61 | thumbnail: string; 62 | edited: boolean; 63 | author_flair_css_class: null; 64 | author_flair_richtext: any[]; 65 | gildings: Gildings; 66 | post_hint: string; 67 | content_categories: null; 68 | is_self: boolean; 69 | mod_note: null; 70 | created: number; 71 | link_flair_type: string; 72 | wls: number; 73 | removed_by_category: null; 74 | banned_by: null; 75 | author_flair_type: string; 76 | domain: string; 77 | allow_live_comments: boolean; 78 | selftext_html: null; 79 | likes: null; 80 | suggested_sort: null; 81 | banned_at_utc: null; 82 | url_overridden_by_dest: string; 83 | view_count: null; 84 | archived: boolean; 85 | no_follow: boolean; 86 | is_crosspostable: boolean; 87 | pinned: boolean; 88 | over_18: boolean; 89 | preview: Preview; 90 | all_awardings: (Allawarding | Allawardings2 | Allawardings3)[]; 91 | awarders: any[]; 92 | media_only: boolean; 93 | can_gild: boolean; 94 | spoiler: boolean; 95 | locked: boolean; 96 | author_flair_text: null; 97 | treatment_tags: any[]; 98 | visited: boolean; 99 | removed_by: null; 100 | num_reports: null; 101 | distinguished: null; 102 | subreddit_id: string; 103 | mod_reason_by: null; 104 | removal_reason: null; 105 | link_flair_background_color: string; 106 | id: string; 107 | is_robot_indexable: boolean; 108 | report_reasons: null; 109 | author: string; 110 | discussion_type: null; 111 | num_comments: number; 112 | send_replies: boolean; 113 | whitelist_status: string; 114 | contest_mode: boolean; 115 | mod_reports: any[]; 116 | author_patreon_flair: boolean; 117 | author_flair_text_color: null; 118 | permalink: string; 119 | parent_whitelist_status: string; 120 | stickied: boolean; 121 | url: string; 122 | subreddit_subscribers: number; 123 | created_utc: number; 124 | num_crossposts: number; 125 | media: Securemedia; 126 | is_video: boolean; 127 | link_flair_template_id?: string; 128 | } 129 | interface Allawardings3 { 130 | giver_coin_reward: null | number; 131 | subreddit_id: null; 132 | is_new: boolean; 133 | days_of_drip_extension: number; 134 | coin_price: number; 135 | id: string; 136 | penny_donate: null | number; 137 | award_sub_type: string; 138 | coin_reward: number; 139 | icon_url: string; 140 | days_of_premium: number; 141 | tiers_by_required_awardings: null; 142 | resized_icons: Source[]; 143 | icon_width: number; 144 | static_icon_width: number; 145 | start_date: null; 146 | is_enabled: boolean; 147 | awardings_required_to_grant_benefits: null; 148 | description: string; 149 | end_date: null; 150 | subreddit_coin_reward: number; 151 | count: number; 152 | static_icon_height: number; 153 | name: string; 154 | resized_static_icons: Source[]; 155 | icon_format: null | string; 156 | icon_height: number; 157 | penny_price: null | number; 158 | award_type: string; 159 | static_icon_url: string; 160 | } 161 | interface Allawardings2 { 162 | giver_coin_reward: number; 163 | subreddit_id: null; 164 | is_new: boolean; 165 | days_of_drip_extension: number; 166 | coin_price: number; 167 | id: string; 168 | penny_donate: number; 169 | award_sub_type: string; 170 | coin_reward: number; 171 | icon_url: string; 172 | days_of_premium: number; 173 | tiers_by_required_awardings: Tiersbyrequiredawardings; 174 | resized_icons: Source[]; 175 | icon_width: number; 176 | static_icon_width: number; 177 | start_date: null; 178 | is_enabled: boolean; 179 | awardings_required_to_grant_benefits: number; 180 | description: string; 181 | end_date: null; 182 | subreddit_coin_reward: number; 183 | count: number; 184 | static_icon_height: number; 185 | name: string; 186 | resized_static_icons: Source[]; 187 | icon_format: string; 188 | icon_height: number; 189 | penny_price: number; 190 | award_type: string; 191 | static_icon_url: string; 192 | } 193 | interface Tiersbyrequiredawardings { 194 | '0': _0; 195 | '5': _0; 196 | '10': _0; 197 | '25': _0; 198 | } 199 | interface _0 { 200 | resized_icons: Source[]; 201 | awardings_required: number; 202 | static_icon: Staticicon; 203 | resized_static_icons: Source[]; 204 | icon: Icon; 205 | } 206 | interface Icon { 207 | url: string; 208 | width: number; 209 | format: string; 210 | height: number; 211 | } 212 | interface Staticicon { 213 | url: string; 214 | width: number; 215 | format: null; 216 | height: number; 217 | } 218 | interface Allawarding { 219 | giver_coin_reward: null; 220 | subreddit_id: null; 221 | is_new: boolean; 222 | days_of_drip_extension: number; 223 | coin_price: number; 224 | id: string; 225 | penny_donate: null; 226 | award_sub_type: string; 227 | coin_reward: number; 228 | icon_url: string; 229 | days_of_premium: number; 230 | tiers_by_required_awardings: null; 231 | resized_icons: Source[]; 232 | icon_width: number; 233 | static_icon_width: number; 234 | start_date: null; 235 | is_enabled: boolean; 236 | awardings_required_to_grant_benefits: null; 237 | description: string; 238 | end_date: null; 239 | subreddit_coin_reward: number; 240 | count: number; 241 | static_icon_height: number; 242 | name: string; 243 | resized_static_icons: Source[]; 244 | icon_format: null; 245 | icon_height: number; 246 | penny_price: null; 247 | award_type: string; 248 | static_icon_url: string; 249 | } 250 | interface Preview { 251 | images: Image[]; 252 | enabled: boolean; 253 | } 254 | interface Image { 255 | source: Source; 256 | resolutions: Source[]; 257 | variants: Variants; 258 | id: string; 259 | } 260 | interface Variants { 261 | obfuscated?: Obfuscated; 262 | nsfw?: Obfuscated; 263 | } 264 | interface Obfuscated { 265 | source: Source; 266 | resolutions: Source[]; 267 | } 268 | interface Source { 269 | url: string; 270 | width: number; 271 | height: number; 272 | } 273 | interface Gildings { 274 | gid_1?: number; 275 | gid_2?: number; 276 | } 277 | interface Securemediaembed { 278 | content: string; 279 | width: number; 280 | scrolling: boolean; 281 | media_domain_url: string; 282 | height: number; 283 | } 284 | interface Securemedia { 285 | oembed: Oembed; 286 | type: string; 287 | } 288 | interface Oembed { 289 | provider_url: string; 290 | title: string; 291 | html: string; 292 | thumbnail_width: number; 293 | height: number; 294 | width: number; 295 | version: string; 296 | author_name: string; 297 | provider_name: string; 298 | thumbnail_url: string; 299 | type: string; 300 | thumbnail_height: number; 301 | author_url: string; 302 | description?: string; 303 | } 304 | interface Mediaembed { 305 | content: string; 306 | width: number; 307 | scrolling: boolean; 308 | height: number; 309 | }" 310 | `; 311 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/samplesToType/index.test.ts: -------------------------------------------------------------------------------- 1 | import { samplesToType } from "."; 2 | 3 | test("generates types and interfaces from samples", () => { 4 | expect(samplesToType([1, { a: 2 }, true, null])).toBe( 5 | `type TypeholeRoot = (boolean | TypeholeRootWrapper2 | null | number); 6 | interface TypeholeRootWrapper2 { 7 | a: number; 8 | }` 9 | ); 10 | expect(samplesToType([1, { a: 2 }, true, null], { useTypeAlias: true })).toBe( 11 | `type TypeholeRoot = (boolean | TypeholeRootWrapper2 | null | number); 12 | type TypeholeRootWrapper2 = { 13 | a: number; 14 | }` 15 | ); 16 | expect(samplesToType([{ a: 2 }])).toBe( 17 | `interface TypeholeRoot { 18 | a: number; 19 | }` 20 | ); 21 | expect(samplesToType([{ a: 2 }, { a: null }])).toBe( 22 | `interface TypeholeRoot { 23 | a: null | number; 24 | }` 25 | ); 26 | 27 | expect(samplesToType([require("./redditResponse.json")])).toMatchSnapshot(); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/samplesToType/index.ts: -------------------------------------------------------------------------------- 1 | import json2ts from "@riku/json-to-ts"; 2 | import { Options } from "@riku/json-to-ts/build/src/model"; 3 | 4 | export function samplesToType( 5 | samples: any[], 6 | jsonToTSOptions?: Options 7 | ): string { 8 | let wrapperType = null; 9 | let samplesWithoutWrapperTypes = []; 10 | for (const sample of samples) { 11 | if (typeof sample === "object" && sample?.__typehole_wrapper_type__) { 12 | wrapperType = sample.__typehole_wrapper_type__; 13 | samplesWithoutWrapperTypes.push(sample.__typehole_value__); 14 | } else { 15 | samplesWithoutWrapperTypes.push(sample); 16 | } 17 | } 18 | 19 | // eslint-disable-next-line @typescript-eslint/naming-convention 20 | const types = json2ts( 21 | { 22 | __typeholeRootWrapper__: samplesWithoutWrapperTypes, 23 | }, 24 | jsonToTSOptions 25 | ).join("\n"); 26 | 27 | let root = types 28 | .match(/__typeholeRootWrapper__:\s(.+)/)![1] 29 | .replace("[];", ";"); 30 | 31 | if (wrapperType) { 32 | root = `${wrapperType}<${root.replace(";", "")}>;`; 33 | } 34 | const result = 35 | `type TypeholeRoot = ${root}\n` + types.split("\n").slice(3).join("\n"); 36 | 37 | if (result.includes("type TypeholeRoot = TypeholeRootWrapper;")) { 38 | return result 39 | .replace("type TypeholeRoot = TypeholeRootWrapper;\n", "") 40 | .replace("TypeholeRootWrapper", "TypeholeRoot"); 41 | } 42 | return result; 43 | } 44 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/wrapIntoRecorder.test.ts: -------------------------------------------------------------------------------- 1 | import { tsquery } from "@phenomnomnominal/tsquery"; 2 | import { getAST } from "../parse/module"; 3 | import { wrapIntoRecorder } from "./wrapIntoRecorder"; 4 | 5 | test("wraps function declaration into a recorder", () => { 6 | const selectedExpression = ` 7 | function App() { 8 | return ( 9 |
10 |
11 | logo 12 |

13 | Edit src/App.tsx and save to{" "} 14 |

15 | 21 | Learn React 22 | 23 |
24 |
25 | ); 26 | } 27 | `; 28 | 29 | const actual = wrapIntoRecorder(0, getAST(selectedExpression)); 30 | expect(actual).toMatchSnapshot(); 31 | }); 32 | 33 | test("wraps objects with inner typeholes", () => { 34 | const ast = getAST(`const foo = { bar: typehole.t1(234) }`); 35 | 36 | const actual = wrapIntoRecorder( 37 | 0, 38 | tsquery.query(ast, "ObjectLiteralExpression")[0] 39 | ); 40 | 41 | expect(actual).toMatchSnapshot(); 42 | }); 43 | 44 | test("wraps expressions into recorder call", () => { 45 | const ast = getAST( 46 | `tsquery.query(ast, "InterfaceDeclaration > Identifier[name='AutoDiscover']")` 47 | ); 48 | const actual = wrapIntoRecorder(0, tsquery.query(ast, "CallExpression")[0]); 49 | expect(actual).toMatchSnapshot(); 50 | }); 51 | 52 | test("wraps literals into recorder calls", () => { 53 | const withAST = (code: string) => getAST(code).getChildAt(0).getChildAt(0); 54 | expect(wrapIntoRecorder(0, withAST(`1 + 3 + 4`))).toMatchSnapshot(); 55 | expect(wrapIntoRecorder(0, withAST(`"moro" + "moro"`))).toMatchSnapshot(); 56 | expect(wrapIntoRecorder(0, withAST(`["moro", "moro"]`))).toMatchSnapshot(); 57 | expect(wrapIntoRecorder(0, withAST(`[() => 3, "moro"]`))).toMatchSnapshot(); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/extension/src/transforms/wrapIntoRecorder.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | function nodeToParameterExpression(node: ts.Node): ts.Expression { 4 | if (ts.isFunctionDeclaration(node)) { 5 | return ts.factory.createFunctionExpression( 6 | node.modifiers, 7 | node.asteriskToken, 8 | node.name, 9 | node.typeParameters, 10 | node.parameters, 11 | node.type, 12 | node.body! 13 | ); 14 | } 15 | 16 | if (ts.isExpressionStatement(node)) { 17 | return node.expression; 18 | } 19 | return node as ts.Expression; 20 | } 21 | 22 | export function wrapIntoRecorder(id: number, node: ts.Node) { 23 | const sourceFile = node.getSourceFile(); 24 | 25 | let rootNode: ts.Node = node; 26 | 27 | while ( 28 | ts.isSourceFile(rootNode) || 29 | ts.SyntaxKind[rootNode.kind] === "SyntaxList" 30 | ) { 31 | rootNode = rootNode.getChildAt(0); 32 | } 33 | 34 | const wrapped = ts.factory.createCallExpression( 35 | ts.factory.createPropertyAccessExpression( 36 | ts.factory.createIdentifier("typehole"), 37 | ts.factory.createIdentifier(`t${id === 0 ? "" : id}`) 38 | ), 39 | undefined, 40 | [nodeToParameterExpression(rootNode)] 41 | ); 42 | 43 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 44 | const result = printer.printNode( 45 | ts.EmitHint.Unspecified, 46 | wrapped, 47 | sourceFile 48 | ); 49 | 50 | return result; 51 | } 52 | -------------------------------------------------------------------------------- /packages/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "es6", 10 | "es2019" 11 | ], 12 | "sourceMap": true, 13 | "rootDir": "src", 14 | "skipLibCheck": true, 15 | "strict": true /* enable all strict type-checking options */ 16 | /* Additional Checks */ 17 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 18 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 19 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | ".vscode-test" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/extension/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | 25 | ## Explore the API 26 | 27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 28 | 29 | ## Run tests 30 | 31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 32 | * Press `F5` to run the tests in a new window with your extension loaded. 33 | * See the output of the test result in the debug console. 34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /packages/runtime/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /packages/runtime/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [10, 12, 14] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.nodejs }} 17 | 18 | - name: Install 19 | run: | 20 | npm install 21 | npm install -g nyc 22 | 23 | - name: Compiles 24 | run: npm run build 25 | 26 | - name: Test w/ Coverage 27 | run: nyc --include=src npm test 28 | 29 | - name: Report 30 | if: matrix.nodejs >= 14 31 | run: | 32 | nyc report --reporter=text-lcov > coverage.lcov 33 | bash <(curl -s https://codecov.io/bash) 34 | env: 35 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 36 | -------------------------------------------------------------------------------- /packages/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /dist 8 | /types 9 | -------------------------------------------------------------------------------- /packages/runtime/license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) TODO_YOUR_NAME (https://TODO_WEBSITE.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /packages/runtime/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typehole", 3 | "version": "1.8.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "typehole", 9 | "version": "1.8.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@types/isomorphic-fetch": "0.0.35", 13 | "isomorphic-fetch": "^3.0.0" 14 | }, 15 | "devDependencies": { 16 | "@rollup/plugin-node-resolve": "8.1.0", 17 | "@types/node": "^14.14.37", 18 | "rollup": "2.21.0", 19 | "rollup-plugin-terser": "6.1.0", 20 | "rollup-plugin-typescript2": "0.27.1", 21 | "ts-node": "8.10.2", 22 | "typescript": "3.9.6", 23 | "uvu": "0.0.19", 24 | "watchlist": "^0.2.3" 25 | }, 26 | "engines": { 27 | "node": ">= 10" 28 | } 29 | }, 30 | "node_modules/@babel/code-frame": { 31 | "version": "7.12.13", 32 | "dev": true, 33 | "license": "MIT", 34 | "dependencies": { 35 | "@babel/highlight": "^7.12.13" 36 | } 37 | }, 38 | "node_modules/@babel/helper-validator-identifier": { 39 | "version": "7.12.11", 40 | "dev": true, 41 | "license": "MIT" 42 | }, 43 | "node_modules/@babel/highlight": { 44 | "version": "7.13.10", 45 | "dev": true, 46 | "license": "MIT", 47 | "dependencies": { 48 | "@babel/helper-validator-identifier": "^7.12.11", 49 | "chalk": "^2.0.0", 50 | "js-tokens": "^4.0.0" 51 | } 52 | }, 53 | "node_modules/@rollup/plugin-node-resolve": { 54 | "version": "8.1.0", 55 | "dev": true, 56 | "license": "MIT", 57 | "dependencies": { 58 | "@rollup/pluginutils": "^3.0.8", 59 | "@types/resolve": "0.0.8", 60 | "builtin-modules": "^3.1.0", 61 | "deep-freeze": "^0.0.1", 62 | "deepmerge": "^4.2.2", 63 | "is-module": "^1.0.0", 64 | "resolve": "^1.14.2" 65 | }, 66 | "engines": { 67 | "node": ">= 8.0.0" 68 | }, 69 | "peerDependencies": { 70 | "rollup": "^1.20.0||^2.0.0" 71 | } 72 | }, 73 | "node_modules/@rollup/pluginutils": { 74 | "version": "3.1.0", 75 | "dev": true, 76 | "license": "MIT", 77 | "dependencies": { 78 | "@types/estree": "0.0.39", 79 | "estree-walker": "^1.0.1", 80 | "picomatch": "^2.2.2" 81 | }, 82 | "engines": { 83 | "node": ">= 8.0.0" 84 | }, 85 | "peerDependencies": { 86 | "rollup": "^1.20.0||^2.0.0" 87 | } 88 | }, 89 | "node_modules/@types/estree": { 90 | "version": "0.0.39", 91 | "dev": true, 92 | "license": "MIT" 93 | }, 94 | "node_modules/@types/isomorphic-fetch": { 95 | "version": "0.0.35", 96 | "license": "MIT" 97 | }, 98 | "node_modules/@types/node": { 99 | "version": "14.14.37", 100 | "dev": true, 101 | "license": "MIT" 102 | }, 103 | "node_modules/@types/resolve": { 104 | "version": "0.0.8", 105 | "dev": true, 106 | "license": "MIT", 107 | "dependencies": { 108 | "@types/node": "*" 109 | } 110 | }, 111 | "node_modules/ansi-styles": { 112 | "version": "3.2.1", 113 | "dev": true, 114 | "license": "MIT", 115 | "dependencies": { 116 | "color-convert": "^1.9.0" 117 | }, 118 | "engines": { 119 | "node": ">=4" 120 | } 121 | }, 122 | "node_modules/arg": { 123 | "version": "4.1.3", 124 | "dev": true, 125 | "license": "MIT" 126 | }, 127 | "node_modules/buffer-from": { 128 | "version": "1.1.1", 129 | "dev": true, 130 | "license": "MIT" 131 | }, 132 | "node_modules/builtin-modules": { 133 | "version": "3.2.0", 134 | "dev": true, 135 | "license": "MIT", 136 | "engines": { 137 | "node": ">=6" 138 | }, 139 | "funding": { 140 | "url": "https://github.com/sponsors/sindresorhus" 141 | } 142 | }, 143 | "node_modules/chalk": { 144 | "version": "2.4.2", 145 | "dev": true, 146 | "license": "MIT", 147 | "dependencies": { 148 | "ansi-styles": "^3.2.1", 149 | "escape-string-regexp": "^1.0.5", 150 | "supports-color": "^5.3.0" 151 | }, 152 | "engines": { 153 | "node": ">=4" 154 | } 155 | }, 156 | "node_modules/color-convert": { 157 | "version": "1.9.3", 158 | "dev": true, 159 | "license": "MIT", 160 | "dependencies": { 161 | "color-name": "1.1.3" 162 | } 163 | }, 164 | "node_modules/color-name": { 165 | "version": "1.1.3", 166 | "dev": true, 167 | "license": "MIT" 168 | }, 169 | "node_modules/commander": { 170 | "version": "2.20.3", 171 | "dev": true, 172 | "license": "MIT" 173 | }, 174 | "node_modules/commondir": { 175 | "version": "1.0.1", 176 | "dev": true, 177 | "license": "MIT" 178 | }, 179 | "node_modules/deep-freeze": { 180 | "version": "0.0.1", 181 | "dev": true, 182 | "license": "public domain" 183 | }, 184 | "node_modules/deepmerge": { 185 | "version": "4.2.2", 186 | "dev": true, 187 | "license": "MIT", 188 | "engines": { 189 | "node": ">=0.10.0" 190 | } 191 | }, 192 | "node_modules/dequal": { 193 | "version": "1.0.1", 194 | "dev": true, 195 | "license": "MIT", 196 | "engines": { 197 | "node": ">=6" 198 | } 199 | }, 200 | "node_modules/diff": { 201 | "version": "4.0.2", 202 | "dev": true, 203 | "license": "BSD-3-Clause", 204 | "engines": { 205 | "node": ">=0.3.1" 206 | } 207 | }, 208 | "node_modules/escape-string-regexp": { 209 | "version": "1.0.5", 210 | "dev": true, 211 | "license": "MIT", 212 | "engines": { 213 | "node": ">=0.8.0" 214 | } 215 | }, 216 | "node_modules/estree-walker": { 217 | "version": "1.0.1", 218 | "dev": true, 219 | "license": "MIT" 220 | }, 221 | "node_modules/find-cache-dir": { 222 | "version": "3.3.1", 223 | "dev": true, 224 | "license": "MIT", 225 | "dependencies": { 226 | "commondir": "^1.0.1", 227 | "make-dir": "^3.0.2", 228 | "pkg-dir": "^4.1.0" 229 | }, 230 | "engines": { 231 | "node": ">=8" 232 | }, 233 | "funding": { 234 | "url": "https://github.com/avajs/find-cache-dir?sponsor=1" 235 | } 236 | }, 237 | "node_modules/find-up": { 238 | "version": "4.1.0", 239 | "dev": true, 240 | "license": "MIT", 241 | "dependencies": { 242 | "locate-path": "^5.0.0", 243 | "path-exists": "^4.0.0" 244 | }, 245 | "engines": { 246 | "node": ">=8" 247 | } 248 | }, 249 | "node_modules/fs-extra": { 250 | "version": "8.1.0", 251 | "dev": true, 252 | "license": "MIT", 253 | "dependencies": { 254 | "graceful-fs": "^4.2.0", 255 | "jsonfile": "^4.0.0", 256 | "universalify": "^0.1.0" 257 | }, 258 | "engines": { 259 | "node": ">=6 <7 || >=8" 260 | } 261 | }, 262 | "node_modules/fsevents": { 263 | "version": "2.1.3", 264 | "dev": true, 265 | "license": "MIT", 266 | "optional": true, 267 | "os": [ 268 | "darwin" 269 | ], 270 | "engines": { 271 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 272 | } 273 | }, 274 | "node_modules/function-bind": { 275 | "version": "1.1.1", 276 | "dev": true, 277 | "license": "MIT" 278 | }, 279 | "node_modules/graceful-fs": { 280 | "version": "4.2.6", 281 | "dev": true, 282 | "license": "ISC" 283 | }, 284 | "node_modules/has": { 285 | "version": "1.0.3", 286 | "dev": true, 287 | "license": "MIT", 288 | "dependencies": { 289 | "function-bind": "^1.1.1" 290 | }, 291 | "engines": { 292 | "node": ">= 0.4.0" 293 | } 294 | }, 295 | "node_modules/has-flag": { 296 | "version": "3.0.0", 297 | "dev": true, 298 | "license": "MIT", 299 | "engines": { 300 | "node": ">=4" 301 | } 302 | }, 303 | "node_modules/is-core-module": { 304 | "version": "2.2.0", 305 | "dev": true, 306 | "license": "MIT", 307 | "dependencies": { 308 | "has": "^1.0.3" 309 | }, 310 | "funding": { 311 | "url": "https://github.com/sponsors/ljharb" 312 | } 313 | }, 314 | "node_modules/is-module": { 315 | "version": "1.0.0", 316 | "dev": true, 317 | "license": "MIT" 318 | }, 319 | "node_modules/isomorphic-fetch": { 320 | "version": "3.0.0", 321 | "license": "MIT", 322 | "dependencies": { 323 | "node-fetch": "^2.6.1", 324 | "whatwg-fetch": "^3.4.1" 325 | } 326 | }, 327 | "node_modules/jest-worker": { 328 | "version": "26.6.2", 329 | "dev": true, 330 | "license": "MIT", 331 | "dependencies": { 332 | "@types/node": "*", 333 | "merge-stream": "^2.0.0", 334 | "supports-color": "^7.0.0" 335 | }, 336 | "engines": { 337 | "node": ">= 10.13.0" 338 | } 339 | }, 340 | "node_modules/jest-worker/node_modules/has-flag": { 341 | "version": "4.0.0", 342 | "dev": true, 343 | "license": "MIT", 344 | "engines": { 345 | "node": ">=8" 346 | } 347 | }, 348 | "node_modules/jest-worker/node_modules/supports-color": { 349 | "version": "7.2.0", 350 | "dev": true, 351 | "license": "MIT", 352 | "dependencies": { 353 | "has-flag": "^4.0.0" 354 | }, 355 | "engines": { 356 | "node": ">=8" 357 | } 358 | }, 359 | "node_modules/js-tokens": { 360 | "version": "4.0.0", 361 | "dev": true, 362 | "license": "MIT" 363 | }, 364 | "node_modules/jsonfile": { 365 | "version": "4.0.0", 366 | "dev": true, 367 | "license": "MIT", 368 | "optionalDependencies": { 369 | "graceful-fs": "^4.1.6" 370 | } 371 | }, 372 | "node_modules/kleur": { 373 | "version": "4.1.4", 374 | "dev": true, 375 | "license": "MIT", 376 | "engines": { 377 | "node": ">=6" 378 | } 379 | }, 380 | "node_modules/locate-path": { 381 | "version": "5.0.0", 382 | "dev": true, 383 | "license": "MIT", 384 | "dependencies": { 385 | "p-locate": "^4.1.0" 386 | }, 387 | "engines": { 388 | "node": ">=8" 389 | } 390 | }, 391 | "node_modules/make-dir": { 392 | "version": "3.1.0", 393 | "dev": true, 394 | "license": "MIT", 395 | "dependencies": { 396 | "semver": "^6.0.0" 397 | }, 398 | "engines": { 399 | "node": ">=8" 400 | }, 401 | "funding": { 402 | "url": "https://github.com/sponsors/sindresorhus" 403 | } 404 | }, 405 | "node_modules/make-error": { 406 | "version": "1.3.6", 407 | "dev": true, 408 | "license": "ISC" 409 | }, 410 | "node_modules/merge-stream": { 411 | "version": "2.0.0", 412 | "dev": true, 413 | "license": "MIT" 414 | }, 415 | "node_modules/mri": { 416 | "version": "1.1.6", 417 | "dev": true, 418 | "license": "MIT", 419 | "engines": { 420 | "node": ">=4" 421 | } 422 | }, 423 | "node_modules/node-fetch": { 424 | "version": "2.6.1", 425 | "license": "MIT", 426 | "engines": { 427 | "node": "4.x || >=6.0.0" 428 | } 429 | }, 430 | "node_modules/p-limit": { 431 | "version": "2.3.0", 432 | "dev": true, 433 | "license": "MIT", 434 | "dependencies": { 435 | "p-try": "^2.0.0" 436 | }, 437 | "engines": { 438 | "node": ">=6" 439 | }, 440 | "funding": { 441 | "url": "https://github.com/sponsors/sindresorhus" 442 | } 443 | }, 444 | "node_modules/p-locate": { 445 | "version": "4.1.0", 446 | "dev": true, 447 | "license": "MIT", 448 | "dependencies": { 449 | "p-limit": "^2.2.0" 450 | }, 451 | "engines": { 452 | "node": ">=8" 453 | } 454 | }, 455 | "node_modules/p-try": { 456 | "version": "2.2.0", 457 | "dev": true, 458 | "license": "MIT", 459 | "engines": { 460 | "node": ">=6" 461 | } 462 | }, 463 | "node_modules/path-exists": { 464 | "version": "4.0.0", 465 | "dev": true, 466 | "license": "MIT", 467 | "engines": { 468 | "node": ">=8" 469 | } 470 | }, 471 | "node_modules/path-parse": { 472 | "version": "1.0.6", 473 | "dev": true, 474 | "license": "MIT" 475 | }, 476 | "node_modules/picomatch": { 477 | "version": "2.2.3", 478 | "dev": true, 479 | "license": "MIT", 480 | "engines": { 481 | "node": ">=8.6" 482 | }, 483 | "funding": { 484 | "url": "https://github.com/sponsors/jonschlinkert" 485 | } 486 | }, 487 | "node_modules/pkg-dir": { 488 | "version": "4.2.0", 489 | "dev": true, 490 | "license": "MIT", 491 | "dependencies": { 492 | "find-up": "^4.0.0" 493 | }, 494 | "engines": { 495 | "node": ">=8" 496 | } 497 | }, 498 | "node_modules/randombytes": { 499 | "version": "2.1.0", 500 | "dev": true, 501 | "license": "MIT", 502 | "dependencies": { 503 | "safe-buffer": "^5.1.0" 504 | } 505 | }, 506 | "node_modules/resolve": { 507 | "version": "1.20.0", 508 | "dev": true, 509 | "license": "MIT", 510 | "dependencies": { 511 | "is-core-module": "^2.2.0", 512 | "path-parse": "^1.0.6" 513 | }, 514 | "funding": { 515 | "url": "https://github.com/sponsors/ljharb" 516 | } 517 | }, 518 | "node_modules/rollup": { 519 | "version": "2.21.0", 520 | "dev": true, 521 | "license": "MIT", 522 | "bin": { 523 | "rollup": "dist/bin/rollup" 524 | }, 525 | "engines": { 526 | "node": ">=10.0.0" 527 | }, 528 | "optionalDependencies": { 529 | "fsevents": "~2.1.2" 530 | } 531 | }, 532 | "node_modules/rollup-plugin-terser": { 533 | "version": "6.1.0", 534 | "dev": true, 535 | "license": "MIT", 536 | "dependencies": { 537 | "@babel/code-frame": "^7.8.3", 538 | "jest-worker": "^26.0.0", 539 | "serialize-javascript": "^3.0.0", 540 | "terser": "^4.7.0" 541 | }, 542 | "peerDependencies": { 543 | "rollup": "^2.0.0" 544 | } 545 | }, 546 | "node_modules/rollup-plugin-typescript2": { 547 | "version": "0.27.1", 548 | "dev": true, 549 | "license": "MIT", 550 | "dependencies": { 551 | "@rollup/pluginutils": "^3.0.8", 552 | "find-cache-dir": "^3.3.1", 553 | "fs-extra": "8.1.0", 554 | "resolve": "1.15.1", 555 | "tslib": "1.11.2" 556 | }, 557 | "peerDependencies": { 558 | "rollup": ">=1.26.3", 559 | "typescript": ">=2.4.0" 560 | } 561 | }, 562 | "node_modules/rollup-plugin-typescript2/node_modules/resolve": { 563 | "version": "1.15.1", 564 | "dev": true, 565 | "license": "MIT", 566 | "dependencies": { 567 | "path-parse": "^1.0.6" 568 | }, 569 | "funding": { 570 | "url": "https://github.com/sponsors/ljharb" 571 | } 572 | }, 573 | "node_modules/sade": { 574 | "version": "1.7.4", 575 | "dev": true, 576 | "license": "MIT", 577 | "dependencies": { 578 | "mri": "^1.1.0" 579 | }, 580 | "engines": { 581 | "node": ">= 6" 582 | } 583 | }, 584 | "node_modules/safe-buffer": { 585 | "version": "5.2.1", 586 | "dev": true, 587 | "funding": [ 588 | { 589 | "type": "github", 590 | "url": "https://github.com/sponsors/feross" 591 | }, 592 | { 593 | "type": "patreon", 594 | "url": "https://www.patreon.com/feross" 595 | }, 596 | { 597 | "type": "consulting", 598 | "url": "https://feross.org/support" 599 | } 600 | ], 601 | "license": "MIT" 602 | }, 603 | "node_modules/semver": { 604 | "version": "6.3.0", 605 | "dev": true, 606 | "license": "ISC", 607 | "bin": { 608 | "semver": "bin/semver.js" 609 | } 610 | }, 611 | "node_modules/serialize-javascript": { 612 | "version": "3.1.0", 613 | "dev": true, 614 | "license": "BSD-3-Clause", 615 | "dependencies": { 616 | "randombytes": "^2.1.0" 617 | } 618 | }, 619 | "node_modules/source-map": { 620 | "version": "0.6.1", 621 | "dev": true, 622 | "license": "BSD-3-Clause", 623 | "engines": { 624 | "node": ">=0.10.0" 625 | } 626 | }, 627 | "node_modules/source-map-support": { 628 | "version": "0.5.19", 629 | "dev": true, 630 | "license": "MIT", 631 | "dependencies": { 632 | "buffer-from": "^1.0.0", 633 | "source-map": "^0.6.0" 634 | } 635 | }, 636 | "node_modules/supports-color": { 637 | "version": "5.5.0", 638 | "dev": true, 639 | "license": "MIT", 640 | "dependencies": { 641 | "has-flag": "^3.0.0" 642 | }, 643 | "engines": { 644 | "node": ">=4" 645 | } 646 | }, 647 | "node_modules/terser": { 648 | "version": "4.8.0", 649 | "dev": true, 650 | "license": "BSD-2-Clause", 651 | "dependencies": { 652 | "commander": "^2.20.0", 653 | "source-map": "~0.6.1", 654 | "source-map-support": "~0.5.12" 655 | }, 656 | "bin": { 657 | "terser": "bin/terser" 658 | }, 659 | "engines": { 660 | "node": ">=6.0.0" 661 | } 662 | }, 663 | "node_modules/totalist": { 664 | "version": "1.1.0", 665 | "dev": true, 666 | "license": "MIT", 667 | "engines": { 668 | "node": ">=6" 669 | } 670 | }, 671 | "node_modules/ts-node": { 672 | "version": "8.10.2", 673 | "dev": true, 674 | "license": "MIT", 675 | "dependencies": { 676 | "arg": "^4.1.0", 677 | "diff": "^4.0.1", 678 | "make-error": "^1.1.1", 679 | "source-map-support": "^0.5.17", 680 | "yn": "3.1.1" 681 | }, 682 | "bin": { 683 | "ts-node": "dist/bin.js", 684 | "ts-node-script": "dist/bin-script.js", 685 | "ts-node-transpile-only": "dist/bin-transpile.js", 686 | "ts-script": "dist/bin-script-deprecated.js" 687 | }, 688 | "engines": { 689 | "node": ">=6.0.0" 690 | }, 691 | "peerDependencies": { 692 | "typescript": ">=2.7" 693 | } 694 | }, 695 | "node_modules/tslib": { 696 | "version": "1.11.2", 697 | "dev": true, 698 | "license": "0BSD" 699 | }, 700 | "node_modules/typescript": { 701 | "version": "3.9.6", 702 | "dev": true, 703 | "license": "Apache-2.0", 704 | "bin": { 705 | "tsc": "bin/tsc", 706 | "tsserver": "bin/tsserver" 707 | }, 708 | "engines": { 709 | "node": ">=4.2.0" 710 | } 711 | }, 712 | "node_modules/universalify": { 713 | "version": "0.1.2", 714 | "dev": true, 715 | "license": "MIT", 716 | "engines": { 717 | "node": ">= 4.0.0" 718 | } 719 | }, 720 | "node_modules/uvu": { 721 | "version": "0.0.19", 722 | "dev": true, 723 | "dependencies": { 724 | "dequal": "^1.0.0", 725 | "diff": "^4.0.2", 726 | "kleur": "^4.0.0", 727 | "sade": "^1.7.3", 728 | "totalist": "^1.1.0" 729 | }, 730 | "bin": { 731 | "uvu": "bin.js" 732 | }, 733 | "engines": { 734 | "node": ">=8" 735 | } 736 | }, 737 | "node_modules/watchlist": { 738 | "version": "0.2.3", 739 | "dev": true, 740 | "license": "MIT", 741 | "dependencies": { 742 | "mri": "^1.1.5" 743 | }, 744 | "bin": { 745 | "watchlist": "bin.js" 746 | }, 747 | "engines": { 748 | "node": ">=8" 749 | } 750 | }, 751 | "node_modules/whatwg-fetch": { 752 | "version": "3.6.2", 753 | "license": "MIT" 754 | }, 755 | "node_modules/yn": { 756 | "version": "3.1.1", 757 | "dev": true, 758 | "license": "MIT", 759 | "engines": { 760 | "node": ">=6" 761 | } 762 | } 763 | }, 764 | "dependencies": { 765 | "@babel/code-frame": { 766 | "version": "7.12.13", 767 | "dev": true, 768 | "requires": { 769 | "@babel/highlight": "^7.12.13" 770 | } 771 | }, 772 | "@babel/helper-validator-identifier": { 773 | "version": "7.12.11", 774 | "dev": true 775 | }, 776 | "@babel/highlight": { 777 | "version": "7.13.10", 778 | "dev": true, 779 | "requires": { 780 | "@babel/helper-validator-identifier": "^7.12.11", 781 | "chalk": "^2.0.0", 782 | "js-tokens": "^4.0.0" 783 | } 784 | }, 785 | "@rollup/plugin-node-resolve": { 786 | "version": "8.1.0", 787 | "dev": true, 788 | "requires": { 789 | "@rollup/pluginutils": "^3.0.8", 790 | "@types/resolve": "0.0.8", 791 | "builtin-modules": "^3.1.0", 792 | "deep-freeze": "^0.0.1", 793 | "deepmerge": "^4.2.2", 794 | "is-module": "^1.0.0", 795 | "resolve": "^1.14.2" 796 | } 797 | }, 798 | "@rollup/pluginutils": { 799 | "version": "3.1.0", 800 | "dev": true, 801 | "requires": { 802 | "@types/estree": "0.0.39", 803 | "estree-walker": "^1.0.1", 804 | "picomatch": "^2.2.2" 805 | } 806 | }, 807 | "@types/estree": { 808 | "version": "0.0.39", 809 | "dev": true 810 | }, 811 | "@types/isomorphic-fetch": { 812 | "version": "0.0.35" 813 | }, 814 | "@types/node": { 815 | "version": "14.14.37", 816 | "dev": true 817 | }, 818 | "@types/resolve": { 819 | "version": "0.0.8", 820 | "dev": true, 821 | "requires": { 822 | "@types/node": "*" 823 | } 824 | }, 825 | "ansi-styles": { 826 | "version": "3.2.1", 827 | "dev": true, 828 | "requires": { 829 | "color-convert": "^1.9.0" 830 | } 831 | }, 832 | "arg": { 833 | "version": "4.1.3", 834 | "dev": true 835 | }, 836 | "buffer-from": { 837 | "version": "1.1.1", 838 | "dev": true 839 | }, 840 | "builtin-modules": { 841 | "version": "3.2.0", 842 | "dev": true 843 | }, 844 | "chalk": { 845 | "version": "2.4.2", 846 | "dev": true, 847 | "requires": { 848 | "ansi-styles": "^3.2.1", 849 | "escape-string-regexp": "^1.0.5", 850 | "supports-color": "^5.3.0" 851 | } 852 | }, 853 | "color-convert": { 854 | "version": "1.9.3", 855 | "dev": true, 856 | "requires": { 857 | "color-name": "1.1.3" 858 | } 859 | }, 860 | "color-name": { 861 | "version": "1.1.3", 862 | "dev": true 863 | }, 864 | "commander": { 865 | "version": "2.20.3", 866 | "dev": true 867 | }, 868 | "commondir": { 869 | "version": "1.0.1", 870 | "dev": true 871 | }, 872 | "deep-freeze": { 873 | "version": "0.0.1", 874 | "dev": true 875 | }, 876 | "deepmerge": { 877 | "version": "4.2.2", 878 | "dev": true 879 | }, 880 | "dequal": { 881 | "version": "1.0.1", 882 | "dev": true 883 | }, 884 | "diff": { 885 | "version": "4.0.2", 886 | "dev": true 887 | }, 888 | "escape-string-regexp": { 889 | "version": "1.0.5", 890 | "dev": true 891 | }, 892 | "estree-walker": { 893 | "version": "1.0.1", 894 | "dev": true 895 | }, 896 | "find-cache-dir": { 897 | "version": "3.3.1", 898 | "dev": true, 899 | "requires": { 900 | "commondir": "^1.0.1", 901 | "make-dir": "^3.0.2", 902 | "pkg-dir": "^4.1.0" 903 | } 904 | }, 905 | "find-up": { 906 | "version": "4.1.0", 907 | "dev": true, 908 | "requires": { 909 | "locate-path": "^5.0.0", 910 | "path-exists": "^4.0.0" 911 | } 912 | }, 913 | "fs-extra": { 914 | "version": "8.1.0", 915 | "dev": true, 916 | "requires": { 917 | "graceful-fs": "^4.2.0", 918 | "jsonfile": "^4.0.0", 919 | "universalify": "^0.1.0" 920 | } 921 | }, 922 | "fsevents": { 923 | "version": "2.1.3", 924 | "dev": true, 925 | "optional": true 926 | }, 927 | "function-bind": { 928 | "version": "1.1.1", 929 | "dev": true 930 | }, 931 | "graceful-fs": { 932 | "version": "4.2.6", 933 | "dev": true 934 | }, 935 | "has": { 936 | "version": "1.0.3", 937 | "dev": true, 938 | "requires": { 939 | "function-bind": "^1.1.1" 940 | } 941 | }, 942 | "has-flag": { 943 | "version": "3.0.0", 944 | "dev": true 945 | }, 946 | "is-core-module": { 947 | "version": "2.2.0", 948 | "dev": true, 949 | "requires": { 950 | "has": "^1.0.3" 951 | } 952 | }, 953 | "is-module": { 954 | "version": "1.0.0", 955 | "dev": true 956 | }, 957 | "isomorphic-fetch": { 958 | "version": "3.0.0", 959 | "requires": { 960 | "node-fetch": "^2.6.1", 961 | "whatwg-fetch": "^3.4.1" 962 | } 963 | }, 964 | "jest-worker": { 965 | "version": "26.6.2", 966 | "dev": true, 967 | "requires": { 968 | "@types/node": "*", 969 | "merge-stream": "^2.0.0", 970 | "supports-color": "^7.0.0" 971 | }, 972 | "dependencies": { 973 | "has-flag": { 974 | "version": "4.0.0", 975 | "dev": true 976 | }, 977 | "supports-color": { 978 | "version": "7.2.0", 979 | "dev": true, 980 | "requires": { 981 | "has-flag": "^4.0.0" 982 | } 983 | } 984 | } 985 | }, 986 | "js-tokens": { 987 | "version": "4.0.0", 988 | "dev": true 989 | }, 990 | "jsonfile": { 991 | "version": "4.0.0", 992 | "dev": true, 993 | "requires": { 994 | "graceful-fs": "^4.1.6" 995 | } 996 | }, 997 | "kleur": { 998 | "version": "4.1.4", 999 | "dev": true 1000 | }, 1001 | "locate-path": { 1002 | "version": "5.0.0", 1003 | "dev": true, 1004 | "requires": { 1005 | "p-locate": "^4.1.0" 1006 | } 1007 | }, 1008 | "make-dir": { 1009 | "version": "3.1.0", 1010 | "dev": true, 1011 | "requires": { 1012 | "semver": "^6.0.0" 1013 | } 1014 | }, 1015 | "make-error": { 1016 | "version": "1.3.6", 1017 | "dev": true 1018 | }, 1019 | "merge-stream": { 1020 | "version": "2.0.0", 1021 | "dev": true 1022 | }, 1023 | "mri": { 1024 | "version": "1.1.6", 1025 | "dev": true 1026 | }, 1027 | "node-fetch": { 1028 | "version": "2.6.1" 1029 | }, 1030 | "p-limit": { 1031 | "version": "2.3.0", 1032 | "dev": true, 1033 | "requires": { 1034 | "p-try": "^2.0.0" 1035 | } 1036 | }, 1037 | "p-locate": { 1038 | "version": "4.1.0", 1039 | "dev": true, 1040 | "requires": { 1041 | "p-limit": "^2.2.0" 1042 | } 1043 | }, 1044 | "p-try": { 1045 | "version": "2.2.0", 1046 | "dev": true 1047 | }, 1048 | "path-exists": { 1049 | "version": "4.0.0", 1050 | "dev": true 1051 | }, 1052 | "path-parse": { 1053 | "version": "1.0.6", 1054 | "dev": true 1055 | }, 1056 | "picomatch": { 1057 | "version": "2.2.3", 1058 | "dev": true 1059 | }, 1060 | "pkg-dir": { 1061 | "version": "4.2.0", 1062 | "dev": true, 1063 | "requires": { 1064 | "find-up": "^4.0.0" 1065 | } 1066 | }, 1067 | "randombytes": { 1068 | "version": "2.1.0", 1069 | "dev": true, 1070 | "requires": { 1071 | "safe-buffer": "^5.1.0" 1072 | } 1073 | }, 1074 | "resolve": { 1075 | "version": "1.20.0", 1076 | "dev": true, 1077 | "requires": { 1078 | "is-core-module": "^2.2.0", 1079 | "path-parse": "^1.0.6" 1080 | } 1081 | }, 1082 | "rollup": { 1083 | "version": "2.21.0", 1084 | "dev": true, 1085 | "requires": { 1086 | "fsevents": "~2.1.2" 1087 | } 1088 | }, 1089 | "rollup-plugin-terser": { 1090 | "version": "6.1.0", 1091 | "dev": true, 1092 | "requires": { 1093 | "@babel/code-frame": "^7.8.3", 1094 | "jest-worker": "^26.0.0", 1095 | "serialize-javascript": "^3.0.0", 1096 | "terser": "^4.7.0" 1097 | } 1098 | }, 1099 | "rollup-plugin-typescript2": { 1100 | "version": "0.27.1", 1101 | "dev": true, 1102 | "requires": { 1103 | "@rollup/pluginutils": "^3.0.8", 1104 | "find-cache-dir": "^3.3.1", 1105 | "fs-extra": "8.1.0", 1106 | "resolve": "1.15.1", 1107 | "tslib": "1.11.2" 1108 | }, 1109 | "dependencies": { 1110 | "resolve": { 1111 | "version": "1.15.1", 1112 | "dev": true, 1113 | "requires": { 1114 | "path-parse": "^1.0.6" 1115 | } 1116 | } 1117 | } 1118 | }, 1119 | "sade": { 1120 | "version": "1.7.4", 1121 | "dev": true, 1122 | "requires": { 1123 | "mri": "^1.1.0" 1124 | } 1125 | }, 1126 | "safe-buffer": { 1127 | "version": "5.2.1", 1128 | "dev": true 1129 | }, 1130 | "semver": { 1131 | "version": "6.3.0", 1132 | "dev": true 1133 | }, 1134 | "serialize-javascript": { 1135 | "version": "3.1.0", 1136 | "dev": true, 1137 | "requires": { 1138 | "randombytes": "^2.1.0" 1139 | } 1140 | }, 1141 | "source-map": { 1142 | "version": "0.6.1", 1143 | "dev": true 1144 | }, 1145 | "source-map-support": { 1146 | "version": "0.5.19", 1147 | "dev": true, 1148 | "requires": { 1149 | "buffer-from": "^1.0.0", 1150 | "source-map": "^0.6.0" 1151 | } 1152 | }, 1153 | "supports-color": { 1154 | "version": "5.5.0", 1155 | "dev": true, 1156 | "requires": { 1157 | "has-flag": "^3.0.0" 1158 | } 1159 | }, 1160 | "terser": { 1161 | "version": "4.8.0", 1162 | "dev": true, 1163 | "requires": { 1164 | "commander": "^2.20.0", 1165 | "source-map": "~0.6.1", 1166 | "source-map-support": "~0.5.12" 1167 | } 1168 | }, 1169 | "totalist": { 1170 | "version": "1.1.0", 1171 | "dev": true 1172 | }, 1173 | "ts-node": { 1174 | "version": "8.10.2", 1175 | "dev": true, 1176 | "requires": { 1177 | "arg": "^4.1.0", 1178 | "diff": "^4.0.1", 1179 | "make-error": "^1.1.1", 1180 | "source-map-support": "^0.5.17", 1181 | "yn": "3.1.1" 1182 | } 1183 | }, 1184 | "tslib": { 1185 | "version": "1.11.2", 1186 | "dev": true 1187 | }, 1188 | "typescript": { 1189 | "version": "3.9.6", 1190 | "dev": true 1191 | }, 1192 | "universalify": { 1193 | "version": "0.1.2", 1194 | "dev": true 1195 | }, 1196 | "uvu": { 1197 | "version": "0.0.19", 1198 | "dev": true, 1199 | "requires": { 1200 | "dequal": "^1.0.0", 1201 | "diff": "^4.0.2", 1202 | "kleur": "^4.0.0", 1203 | "sade": "^1.7.3", 1204 | "totalist": "^1.1.0" 1205 | } 1206 | }, 1207 | "watchlist": { 1208 | "version": "0.2.3", 1209 | "dev": true, 1210 | "requires": { 1211 | "mri": "^1.1.5" 1212 | } 1213 | }, 1214 | "whatwg-fetch": { 1215 | "version": "3.6.2" 1216 | }, 1217 | "yn": { 1218 | "version": "3.1.1", 1219 | "dev": true 1220 | } 1221 | } 1222 | } 1223 | -------------------------------------------------------------------------------- /packages/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.8.0", 3 | "name": "typehole", 4 | "repository": "rikukissa/typehole", 5 | "description": "Turn runtime types into static typescript types automatically", 6 | "unpkg": "dist/index.min.js", 7 | "main": "dist/index.js", 8 | "module": "dist/index.mjs", 9 | "exports": { 10 | "require": "./dist/index.js", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "types": "types/index.d.ts", 14 | "license": "MIT", 15 | "author": { 16 | "name": "Riku Rouvila" 17 | }, 18 | "files": [ 19 | "dist", 20 | "types" 21 | ], 22 | "engines": { 23 | "node": ">= 10" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c", 27 | "prepublishOnly": "npm run build", 28 | "test": "uvu -r ts-node/register test", 29 | "test:watch": "watchlist src test -- npm test" 30 | }, 31 | "keywords": [ 32 | "TODO", 33 | "module", 34 | "keywords" 35 | ], 36 | "devDependencies": { 37 | "@rollup/plugin-node-resolve": "8.1.0", 38 | "@types/node": "^14.14.37", 39 | "rollup": "2.21.0", 40 | "rollup-plugin-terser": "6.1.0", 41 | "rollup-plugin-typescript2": "0.27.1", 42 | "ts-node": "8.10.2", 43 | "typescript": "3.9.6", 44 | "uvu": "0.0.19", 45 | "watchlist": "^0.2.3" 46 | }, 47 | "dependencies": { 48 | "@types/isomorphic-fetch": "0.0.35", 49 | "isomorphic-fetch": "^3.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/runtime/readme.md: -------------------------------------------------------------------------------- 1 | # Typehole runtime 2 | 3 | A runtime library for [Typehole](https://github.com/rikukissa/typehole). 4 | 5 | --- 6 | 7 | # Template: TypeScript Module [![CI](https://github.com/lukeed/typescript-module/workflows/CI/badge.svg)](https://github.com/lukeed/typescript-module/actions) [![codecov](https://badgen.now.sh/codecov/c/github/lukeed/typescript-module)](https://codecov.io/gh/lukeed/typescript-module) 8 | 9 | This is a [clonable template repository](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) for authoring a `npm` module with TypeScript. Out of the box, it: 10 | 11 | - Provides minimally-viable `tsconfig.json` settings 12 | - Scaffolds a silly arithmetic module (`src/index.ts`) 13 | - Scaffolds test suites for full test coverage (`test/index.ts`) 14 | - Scaffolds a GitHub Action for Code Integration (CI) that: 15 | - checks if compilation is successful 16 | - runs the test suite(s) 17 | - reports test coverage 18 | - Generates type definitions (`types/*.d.ts`) 19 | - Generates multiple distribution formats: 20 | - ES Module (`dist/index.mjs`) 21 | - CommonJS (`dist/index.js`) 22 | - UMD (`dist/index.min.js`) 23 | 24 | All configuration is accessible via the `rollup.config.js` and a few `package.json` keys: 25 | 26 | - `name` — the name of your module 27 | - `main` — the destination file for your CommonJS build 28 | - `module` — the destination file for your ESM build (optional but recommended) 29 | - `unpkg` — the destination file for your UMD build (optional for [unpkg.com](https://unpkg.com/)) 30 | - `umd:name` — the UMD global name for your module (optional) 31 | 32 | ## Setup 33 | 34 | 1. [Clone this template](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) 35 | 2. Replace all instances of `TODO` within the `license` and `package.json` files 36 | 3. Create [CodeCov](https://codecov.io) account (free for OSS) 37 | 4. Copy the provided CodeCov token as the `CODECOV_TOKEN` [repository secret](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets-for-a-repository) (for CI reporting) 38 | 5. Replace `src/index.ts` and `test/index.ts` with your own code! 🎉 39 | 40 | ## Commands 41 | 42 | ### build 43 | 44 | Builds your module for distribution in multiple formats (ESM, CommonJS, and UMD). 45 | 46 | ```sh 47 | $ npm run build 48 | ``` 49 | 50 | ### test 51 | 52 | Runs your test suite(s) (`/tests/**`) against your source code (`/src/**`).
Doing so allows for accurate code coverage. 53 | 54 | > **Note:** Coverage is only collected and reported through the "CI" Github Action (`.github/workflows/ci.yml`). 55 | 56 | ```sh 57 | $ npm test 58 | ``` 59 | 60 | ## Publishing 61 | 62 | > **Important:** Please finish [Setup](#setup) before continuing! 63 | 64 | Once all `TODO` notes have been updated & your new module is ready to be shared, all that's left to do is decide its new version — AKA, do the changes consitute a `patch`, `minor`, or `major` release? 65 | 66 | Once decided, you can run the following: 67 | 68 | ```sh 69 | $ npm version && git push origin master --tags && npm publish 70 | # Example: 71 | # npm version patch && git push origin master --tags && npm publish 72 | ``` 73 | 74 | This command sequence will: 75 | 76 | - version your module, updating the `package.json` "version" 77 | - create and push a `git` tag (matching the new version) to your repository 78 | - build your module (via the `prepublishOnly` script) 79 | - publish the module to the npm registry 80 | 81 | ## License 82 | 83 | MIT © [Luke Edwards](https://lukeed.com) 84 | -------------------------------------------------------------------------------- /packages/runtime/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "rollup-plugin-typescript2"; 3 | import { terser } from "rollup-plugin-terser"; 4 | import pkg from "./package.json"; 5 | 6 | export default { 7 | input: "src/index.ts", 8 | output: [ 9 | { 10 | format: "cjs", 11 | file: pkg.main, 12 | sourcemap: false, 13 | }, 14 | { 15 | format: "esm", 16 | file: pkg.module, 17 | sourcemap: false, 18 | }, 19 | { 20 | name: pkg["umd:name"] || pkg.name, 21 | format: "umd", 22 | file: pkg.unpkg, 23 | sourcemap: false, 24 | plugins: [terser()], 25 | }, 26 | ], 27 | external: [ 28 | ...require("module").builtinModules, 29 | ...Object.keys(pkg.dependencies || {}), 30 | ...Object.keys(pkg.peerDependencies || {}), 31 | ], 32 | plugins: [ 33 | resolve(), 34 | typescript({ 35 | useTsconfigDeclarationDir: true, 36 | }), 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /packages/runtime/src/index.ts: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-fetch"; 2 | 3 | function serialize(value: any) { 4 | let serialized: string | null = null; 5 | try { 6 | serialized = JSON.stringify(value); 7 | } catch (error) {} 8 | return serialized; 9 | } 10 | 11 | function isPlainObject(value: any) { 12 | return typeof value === "object" && value.toString() === "[object Object]"; 13 | } 14 | 15 | type HoleId = string | symbol | number; 16 | 17 | let config = { 18 | extensionHost: "http://localhost:17341", 19 | }; 20 | 21 | export function configure(newConfig: Partial) { 22 | config = { ...config, ...newConfig }; 23 | } 24 | 25 | function sendUnserializable(holeId: HoleId) { 26 | return fetch(`${config.extensionHost}/unserializable`, { 27 | method: "POST", 28 | mode: "cors", 29 | body: JSON.stringify({ 30 | id: holeId, 31 | }), 32 | headers: { 33 | "Content-Type": "application/json", 34 | }, 35 | }).catch((err) => console.log(err.message)); 36 | } 37 | 38 | // This is here so that multiple types would not be edited simultaniously 39 | let sampleQueue: Promise = Promise.resolve(); 40 | function sendSample(holeId: HoleId, input: any) { 41 | sampleQueue = sampleQueue.then(() => 42 | fetch(`${config.extensionHost}/samples`, { 43 | method: "POST", 44 | mode: "cors", 45 | body: JSON.stringify({ 46 | id: holeId, 47 | sample: input, 48 | }), 49 | headers: { 50 | "Content-Type": "application/json", 51 | }, 52 | }).catch((err) => console.log(err.message)) 53 | ); 54 | return sampleQueue; 55 | } 56 | 57 | async function solveWrapperTypes(value: any) { 58 | if (typeof value?.then === "function") { 59 | return { 60 | __typehole_wrapper_type__: "Promise", 61 | __typehole_value__: await value, 62 | }; 63 | } 64 | return value; 65 | } 66 | 67 | function typeholeFactory(id: HoleId) { 68 | const emitSample = sendSample; 69 | let previousValue: string | null = null; 70 | 71 | return function typehole(input: T): T { 72 | solveWrapperTypes(input).then((withWrapperTypes) => { 73 | const serialized = serialize(withWrapperTypes); 74 | 75 | if (serialized === previousValue) { 76 | return input; 77 | } 78 | 79 | previousValue = serialized; 80 | 81 | if ( 82 | !serialized || 83 | (serialized === "{}" && !isPlainObject(withWrapperTypes)) 84 | ) { 85 | console.info("Typehole:", "Cannot serialize value", { 86 | input, 87 | serialized, 88 | }); 89 | sendUnserializable(id); 90 | } else { 91 | emitSample(id, withWrapperTypes); 92 | } 93 | }); 94 | 95 | return input; 96 | }; 97 | } 98 | 99 | const holes: Record> = {}; 100 | 101 | export default new Proxy>>( 102 | {} as any, 103 | { 104 | get: function (target, prop, receiver) { 105 | if (/^t([0-9]+)?$/.test(prop.toString())) { 106 | if (!holes[prop as string]) { 107 | holes[prop as string] = typeholeFactory(prop); 108 | } 109 | 110 | return holes[prop as string]; 111 | } 112 | 113 | return Reflect.get(target, prop, receiver); 114 | }, 115 | } 116 | ); 117 | -------------------------------------------------------------------------------- /packages/runtime/test/index.ts: -------------------------------------------------------------------------------- 1 | import { suite } from "uvu"; 2 | 3 | const test = suite("typehole runtime"); 4 | 5 | test("placeholder", () => {}); 6 | 7 | test.run(); 8 | -------------------------------------------------------------------------------- /packages/runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "transpileOnly": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | }, 8 | "include": [ 9 | "test/**/*" 10 | ] 11 | }, 12 | "compilerOptions": { 13 | "target": "ES6", 14 | "module": "esnext", 15 | "declaration": true, 16 | "esModuleInterop": true, 17 | "declarationDir": "types", 18 | "forceConsistentCasingInFileNames": true, 19 | "moduleResolution": "node", 20 | "strictNullChecks": true, 21 | "noImplicitAny": true, 22 | "strict": true 23 | }, 24 | "include": [ 25 | "@types/**/*", 26 | "src/**/*" 27 | ], 28 | "exclude": [ 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /publish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd packages/extension 6 | npm run publish-extension 7 | cd ../runtime 8 | npm publish --------------------------------------------------------------------------------