From 8973313c505b8984ce16fa4eb99d1b882079328d Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Tue, 18 May 2021 20:12:11 +0300 Subject: [PATCH 1/7] implement following types to different modules --- README.md | 4 +- packages/extension/package-lock.json | 189 +++++++++++++++++- packages/extension/package.json | 2 +- packages/extension/src/code-action.ts | 1 - packages/extension/src/editor/utils.ts | 51 +++++ packages/extension/src/ensureRuntime.ts | 58 +----- packages/extension/src/extension.ts | 20 +- packages/extension/src/listener.ts | 103 ++++++++-- packages/extension/src/parse/module.ts | 59 +++++- .../src/transforms/insertTypes/index.ts | 61 +++++- 10 files changed, 454 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index fb627bd..a3ff765 100644 --- a/README.md +++ b/README.md @@ -96,10 +96,12 @@ From 1.4.0 forward also Promises are supported. All other values (functions etc. ## Release Notes -## [Unreleased] +## [1.5.1] - 2021-05-18 ### Fixed +- Interface not updating when it was in a different file than the typehole +- Types not updating when some other file was focused in the editor - `typehole.tNaN` [issue](https://github.com/rikukissa/typehole/issues/7) when there have been typeholes with a non `t` format ## [1.5.0] - 2021-05-15 diff --git a/packages/extension/package-lock.json b/packages/extension/package-lock.json index 5ed5aab..208205f 100644 --- a/packages/extension/package-lock.json +++ b/packages/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "typehole", - "version": "1.4.3", + "version": "1.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "typehole", - "version": "1.4.3", + "version": "1.5.1", "dependencies": { "@phenomnomnominal/tsquery": "^4.1.1", "@types/esquery": "^1.0.1", @@ -823,6 +823,7 @@ "jest-resolve": "^26.6.2", "jest-util": "^26.6.2", "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", @@ -2665,7 +2666,8 @@ "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1" + "optionator": "^0.8.1", + "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -4588,6 +4590,7 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", "jest-serializer": "^26.6.2", @@ -5096,6 +5099,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -5574,7 +5578,182 @@ "treeverse", "validate-npm-package-name", "which", - "write-file-atomic" + "write-file-atomic", + "@npmcli/disparity-colors", + "@npmcli/git", + "@npmcli/installed-package-contents", + "@npmcli/map-workspaces", + "@npmcli/metavuln-calculator", + "@npmcli/move-file", + "@npmcli/name-from-folder", + "@npmcli/node-gyp", + "@npmcli/promise-spawn", + "@tootallnate/once", + "agent-base", + "agentkeepalive", + "aggregate-error", + "ajv", + "ansi-regex", + "ansi-styles", + "aproba", + "are-we-there-yet", + "asap", + "asn1", + "assert-plus", + "asynckit", + "aws-sign2", + "aws4", + "balanced-match", + "bcrypt-pbkdf", + "bin-links", + "binary-extensions", + "brace-expansion", + "builtins", + "caseless", + "cidr-regex", + "clean-stack", + "clone", + "cmd-shim", + "code-point-at", + "color-convert", + "color-name", + "colors", + "combined-stream", + "common-ancestor-path", + "concat-map", + "console-control-strings", + "core-util-is", + "dashdash", + "debug", + "debuglog", + "defaults", + "delayed-stream", + "delegates", + "depd", + "dezalgo", + "diff", + "ecc-jsbn", + "emoji-regex", + "encoding", + "env-paths", + "err-code", + "extend", + "extsprintf", + "fast-deep-equal", + "fast-json-stable-stringify", + "forever-agent", + "form-data", + "fs-minipass", + "fs.realpath", + "function-bind", + "gauge", + "getpass", + "har-schema", + "har-validator", + "has", + "has-flag", + "has-unicode", + "http-cache-semantics", + "http-proxy-agent", + "http-signature", + "https-proxy-agent", + "humanize-ms", + "iconv-lite", + "ignore-walk", + "imurmurhash", + "indent-string", + "infer-owner", + "inflight", + "inherits", + "ip", + "ip-regex", + "is-core-module", + "is-fullwidth-code-point", + "is-lambda", + "is-typedarray", + "isarray", + "isexe", + "isstream", + "jsbn", + "json-schema", + "json-schema-traverse", + "json-stringify-nice", + "json-stringify-safe", + "jsonparse", + "jsprim", + "just-diff", + "just-diff-apply", + "lru-cache", + "mime-db", + "mime-types", + "minimatch", + "minipass-collect", + "minipass-fetch", + "minipass-flush", + "minipass-json-stream", + "minipass-sized", + "minizlib", + "mute-stream", + "normalize-package-data", + "npm-bundled", + "npm-install-checks", + "npm-normalize-package-bin", + "npm-packlist", + "number-is-nan", + "oauth-sign", + "object-assign", + "once", + "p-map", + "path-is-absolute", + "path-parse", + "performance-now", + "proc-log", + "process-nextick-args", + "promise-all-reject-late", + "promise-call-limit", + "promise-inflight", + "promise-retry", + "promzard", + "psl", + "punycode", + "qs", + "read-cmd-shim", + "readable-stream", + "request", + "resolve", + "retry", + "safe-buffer", + "safer-buffer", + "set-blocking", + "signal-exit", + "smart-buffer", + "socks", + "socks-proxy-agent", + "spdx-correct", + "spdx-exceptions", + "spdx-expression-parse", + "spdx-license-ids", + "sshpk", + "string_decoder", + "string-width", + "stringify-package", + "strip-ansi", + "supports-color", + "tunnel-agent", + "tweetnacl", + "typedarray-to-buffer", + "unique-filename", + "unique-slug", + "uri-js", + "util-deprecate", + "uuid", + "validate-npm-package-license", + "verror", + "walk-up-path", + "wcwidth", + "wide-align", + "wrappy", + "yallist" ], "dependencies": { "@npmcli/arborist": "^2.4.0", @@ -6132,6 +6311,7 @@ "inBundle": true, "license": "MIT", "dependencies": { + "colors": "^1.1.2", "object-assign": "^4.1.0", "string-width": "^4.2.0" }, @@ -7105,6 +7285,7 @@ "inBundle": true, "license": "MIT", "dependencies": { + "encoding": "^0.1.12", "minipass": "^3.1.0", "minipass-sized": "^1.0.3", "minizlib": "^2.0.0" diff --git a/packages/extension/package.json b/packages/extension/package.json index 714f491..29e6e2f 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -4,7 +4,7 @@ "publisher": "rikurouvila", "description": "🧪 Take samples of runtime values and turn them into type definitions automatically", "repository": "https://github.com/rikukissa/typehole", - "version": "1.5.0", + "version": "1.5.1", "private": true, "icon": "images/logo.png", "galleryBanner": { diff --git a/packages/extension/src/code-action.ts b/packages/extension/src/code-action.ts index f633639..88c76b4 100644 --- a/packages/extension/src/code-action.ts +++ b/packages/extension/src/code-action.ts @@ -1,5 +1,4 @@ import * as vscode from "vscode"; -import * as ts from "typescript"; import { isValidSelection } from "./parse/expression"; import { getAST } from "./parse/module"; import { diff --git a/packages/extension/src/editor/utils.ts b/packages/extension/src/editor/utils.ts index b6564d7..ce629ff 100644 --- a/packages/extension/src/editor/utils.ts +++ b/packages/extension/src/editor/utils.ts @@ -1,5 +1,6 @@ import * as ts from "typescript"; import * as vscode from "vscode"; +import { getConfiguration } from "../config"; import { getNodeEndPosition, getNodeStartPosition } from "../parse/module"; export const getEditorRange = (node: ts.Node) => { @@ -20,3 +21,53 @@ export function getProjectURI() { export function getProjectPath() { return getProjectURI()?.path; } + +export async function getPackageJSONDirectories() { + const include = new vscode.RelativePattern( + getProjectURI()!, + "**/package.json" + ); + + const files = await vscode.workspace.findFiles(include); + + // Done like this as findFiles didn't respect the exclude parameter + return files.filter((f) => !f.path.includes("node_modules")); +} + +export async function resolveProjectRoot( + document: vscode.TextDocument, + options: vscode.Uri[] +) { + const config = getConfiguration("", document.uri); + const answer = await vscode.window.showQuickPick( + options.map((o) => o.path.replace("/package.json", "")), + { + placeHolder: "Where should the runtime package be installed?", + } + ); + + if (answer) { + config.update( + "typehole.runtime.projectPath", + answer, + vscode.ConfigurationTarget.Workspace + ); + return answer; + } +} + +export async function getProjectRoot(document: vscode.TextDocument) { + const config = getConfiguration("", document.uri); + + const packageRoots = await getPackageJSONDirectories(); + + let projectPath = getProjectPath(); + + if (packageRoots.length > 1) { + return ( + config.projectPath || (await resolveProjectRoot(document, packageRoots)) + ); + } + + return projectPath; +} diff --git a/packages/extension/src/ensureRuntime.ts b/packages/extension/src/ensureRuntime.ts index 142ac54..262d61c 100644 --- a/packages/extension/src/ensureRuntime.ts +++ b/packages/extension/src/ensureRuntime.ts @@ -1,9 +1,9 @@ +import { install, setPackageManager, setRootDir } from "lmify"; import * as vscode from "vscode"; -import { install, setRootDir, setPackageManager } from "lmify"; -import { log, error } from "./logger"; -import { getProjectPath, getProjectURI } from "./editor/utils"; import { getConfiguration, PackageManager } from "./config"; +import { getProjectRoot } from "./editor/utils"; +import { error, log } from "./logger"; /* * A bit of a hack as require.resolve doesn't update it's cache @@ -12,19 +12,6 @@ import { getConfiguration, PackageManager } from "./config"; let runtimeWasInstalledWhileExtensionIsRunning = false; - -async function getPackageJSONDirectories() { - const include = new vscode.RelativePattern( - getProjectURI()!, - "**/package.json" - ); - - const files = await vscode.workspace.findFiles(include); - - // Done like this as findFiles didn't respect the exclude parameter - return files.filter((f) => !f.path.includes("node_modules")); -} - async function detectPackageManager(): Promise { const npmLocks = await vscode.workspace.findFiles("package-lock.json"); const yarnLocks = await vscode.workspace.findFiles("yarn.lock"); @@ -38,28 +25,6 @@ async function detectPackageManager(): Promise { } } -async function resolveProjectRoot( - document: vscode.TextDocument, - options: vscode.Uri[] -) { - const config = getConfiguration("", document.uri); - const answer = await vscode.window.showQuickPick( - options.map((o) => o.path.replace("/package.json", "")), - { - placeHolder: "Where should the runtime package be installed?", - } - ); - - if (answer) { - config.update( - "typehole.runtime.projectPath", - answer, - vscode.ConfigurationTarget.Workspace - ); - return answer; - } -} - async function getPackageManager(document: vscode.TextDocument) { const config = getConfiguration("", document.uri); if (config.packageManager) { @@ -90,7 +55,7 @@ function isRuntimeInstalled(projectRoot: string) { try { log("Searching for runtime library in", projectRoot); require.resolve("typehole", { - paths: [projectRoot] + paths: [projectRoot], }); return true; } catch (error) { @@ -121,19 +86,15 @@ export async function ensureRuntime() { } setPackageManager(packageManager); - const packageRoots = await getPackageJSONDirectories(); - let projectPath = getProjectPath(); - - if (packageRoots.length > 1) { - projectPath = - config.projectPath || (await resolveProjectRoot(document, packageRoots)); - } + const projectPath = await getProjectRoot(document); if (!projectPath) { return; } - const installed = isRuntimeInstalled(projectPath) || runtimeWasInstalledWhileExtensionIsRunning; + const installed = + isRuntimeInstalled(projectPath) || + runtimeWasInstalledWhileExtensionIsRunning; if (!installed && config.autoInstall) { installing = true; @@ -165,5 +126,4 @@ export async function ensureRuntime() { "yarn add typehole"`); return; } - -} \ No newline at end of file +} diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 8ac0ab4..2a605f6 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -57,15 +57,17 @@ export function getPlaceholderTypeName(document: ts.SourceFile) { while (results.length > 0) { n++; - results = tsquery.query( - document, - `TypeAliasDeclaration > Identifier[name="AutoDiscovered${n}"]` - ).concat( - tsquery.query( + results = tsquery + .query( document, - `InterfaceDeclaration > Identifier[name="AutoDiscovered${n}"]` + `TypeAliasDeclaration > Identifier[name="AutoDiscovered${n}"]` ) - ); + .concat( + tsquery.query( + document, + `InterfaceDeclaration > Identifier[name="AutoDiscovered${n}"]` + ) + ); } return "AutoDiscovered" + (n === 0 ? "" : n); @@ -153,7 +155,9 @@ export async function activate(context: vscode.ExtensionContext) { const allHolesRemoved = previousState.holes.length > 0 && newState.holes.length === 0; - const shouldEnsureRuntime = previousState.holes.length !== newState.holes.length && newState.holes.length > 0 + const shouldEnsureRuntime = + previousState.holes.length !== newState.holes.length && + newState.holes.length > 0; previousState = newState; diff --git a/packages/extension/src/listener.ts b/packages/extension/src/listener.ts index 8cfff11..18e8fd6 100644 --- a/packages/extension/src/listener.ts +++ b/packages/extension/src/listener.ts @@ -1,15 +1,19 @@ +import { tsquery } from "@phenomnomnominal/tsquery"; import f from "fastify"; +import { join } from "path"; import * as ts from "typescript"; import * as vscode from "vscode"; -import { getEditorRange } from "./editor/utils"; +import { getEditorRange, getProjectRoot } from "./editor/utils"; import { error, log } from "./logger"; -import { findTypeholes, getAST } from "./parse/module"; -import { addSample, addWarning } from "./state"; +import { findTypeholes, getAST, resolveImportPath } from "./parse/module"; +import { addSample, addWarning, getHole } from "./state"; import { + findDeclarationWithName, getAllDependencyTypeDeclarations, getTypeAliasForId, + getTypeReferenceNameForId, } from "./transforms/insertTypes"; import { samplesToType } from "./transforms/samplesToType"; @@ -91,40 +95,99 @@ export async function stopListenerServer() { } async function onTypeExtracted(id: string, types: string) { - const editor = vscode.window.activeTextEditor; - const document = editor?.document; - if (!editor || !document) { + const hole = getHole(id); + + if (!hole) { + error("Hole", id, "was not found. This is not supposed to happen"); return; } - const ast = getAST(editor.document.getText()); - const typeAliasNode = getTypeAliasForId(id, ast); + let document = await vscode.workspace.openTextDocument( + vscode.Uri.file(hole.fileName) + ); + + if (!document) { + return; + } + + let ast = getAST(document.getText()); + + let typeAliasNode = getTypeAliasForId(id, ast); + if (!typeAliasNode) { return; } - const typeName = typeAliasNode.getText(); + const typeName = getTypeReferenceNameForId(id, ast)!; + const typeIsImportedFromAnotherFile = ts.isImportDeclaration(typeAliasNode); + /* + * Type is imported from another file + */ + if (typeIsImportedFromAnotherFile) { + const relativePath = tsquery(typeAliasNode, "StringLiteral")[0] + ?.getText() + .replace(/["']/g, ""); + + const projectRoot = await getProjectRoot(document); + + if (!projectRoot) { + return error("No project root was found when resolving module import"); + } + + const absolutePath = resolveImportPath( + projectRoot, + relativePath, + document.uri.path + )!; + + try { + document = await vscode.workspace.openTextDocument( + vscode.Uri.file(absolutePath) + ); + } catch (err) { + console.log(err); + + return error( + "Failed to open the document the imported type is referring to", + absolutePath, + err.message + ); + } + + ast = getAST(document.getText()); + typeAliasNode = findDeclarationWithName(typeName, ast); + if (!typeAliasNode) { + return; + } + } + + const exported = tsquery(typeAliasNode.parent, "ExportKeyword").length > 0; const existingDeclarations = getAllDependencyTypeDeclarations( typeAliasNode.parent ); - const typesToBeInserted = types.replace("TypeholeRoot", typeName).trim(); + const typesToBeInserted = + (exported ? "export " : "") + + types.replace("TypeholeRoot", typeName).trim(); - await editor.edit((editBuilder) => { - existingDeclarations.forEach((node) => { - const range = getEditorRange(node); - editBuilder.delete(range); - }); + const workEdits = new vscode.WorkspaceEdit(); - editBuilder.insert( - getEditorRange(typeAliasNode.parent).start, - typesToBeInserted - ); + existingDeclarations.forEach((node) => { + const range = getEditorRange(node); + workEdits.delete(document?.uri!, range); }); + workEdits.insert( + document.uri, + getEditorRange(typeAliasNode!.parent).start, + typesToBeInserted + ); + console.log(workEdits); + + vscode.workspace.applyEdit(workEdits); + try { - // TODO We could also only format the types we added await vscode.commands.executeCommand("editor.action.formatDocument"); } catch (err) { error("Formatting the document failed", err.message); diff --git a/packages/extension/src/parse/module.ts b/packages/extension/src/parse/module.ts index d6c6657..dd406f1 100644 --- a/packages/extension/src/parse/module.ts +++ b/packages/extension/src/parse/module.ts @@ -6,6 +6,51 @@ export function findTypeHoleImports(ast: ts.Node) { .query(ast, "ImportDeclaration > StringLiteral[text='typehole']") .map((s) => s.parent); } + +export function resolveImportPath( + projectRoot: string, + moduleName: string, + containingFile: string +) { + const configFileName = ts.findConfigFile( + projectRoot, + ts.sys.fileExists, + "tsconfig.json" + ); + + if (!configFileName) { + return null; + } + + const configFile = ts.readConfigFile(configFileName, ts.sys.readFile); + + const compilerOptions = ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + "./" + ); + + function fileExists(fileName: string): boolean { + return ts.sys.fileExists(fileName); + } + + function readFile(fileName: string): string | undefined { + return ts.sys.readFile(fileName); + } + + const result = ts.resolveModuleName( + moduleName, + containingFile, + compilerOptions.options, + { + fileExists, + readFile, + } + ); + + return result.resolvedModule!.resolvedFileName; +} + export function findLastImport(ast: ts.Node) { const imports = tsquery.query(ast, "ImportDeclaration"); return imports[imports.length - 1]; @@ -34,7 +79,6 @@ export function getTypeHoleImport() { ); } - export function printAST(ast: ts.Node, sourceFile?: ts.SourceFile) { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); @@ -83,3 +127,16 @@ export function someParentIs( } return someParentIs(node.parent, test); } + +export function getParentWithType( + node: ts.Node, + kind: ts.SyntaxKind +): T | null { + if (!node.parent) { + return null; + } + if (node.kind === kind) { + return node as unknown as T; + } + return getParentWithType(node.parent, kind); +} diff --git a/packages/extension/src/transforms/insertTypes/index.ts b/packages/extension/src/transforms/insertTypes/index.ts index 8f37608..afcec25 100644 --- a/packages/extension/src/transforms/insertTypes/index.ts +++ b/packages/extension/src/transforms/insertTypes/index.ts @@ -1,8 +1,23 @@ import * as ts from "typescript"; import { tsquery } from "@phenomnomnominal/tsquery"; -import { findTypeholes, printAST } from "../../parse/module"; +import { findTypeholes, getParentWithType, printAST } from "../../parse/module"; import { unique } from "../../parse/utils"; +function findDeclarationInImportedDeclarations( + name: string, + ast: ts.Node +): ts.ImportDeclaration | null { + return tsquery + .query(ast, `ImportSpecifier > Identifier[name="${name}"]`) + .concat(tsquery.query(ast, `ImportClause > Identifier[name="${name}"]`)) + .map((node) => + getParentWithType( + node, + ts.SyntaxKind.ImportDeclaration + ) + )[0]; +} + function findDeclarationsWithName(name: string, ast: ts.Node) { return tsquery.query( ast, @@ -10,10 +25,22 @@ function findDeclarationsWithName(name: string, ast: ts.Node) { ); } -function findDeclarationWithName(name: string, ast: ts.Node): ts.Node | null { +export function findDeclarationWithName( + name: string, + ast: ts.Node +): + | ts.TypeAliasDeclaration + | ts.InterfaceDeclaration + | ts.ImportDeclaration + | null { const results = findDeclarationsWithName(name, ast); if (results.length === 0) { - return null; + const importStatement = findDeclarationInImportedDeclarations(name, ast); + if (!importStatement) { + return null; + } + + return importStatement; } return results[0]; } @@ -126,7 +153,10 @@ export function findAllDependencyTypeDeclarations( return []; } -export function getTypeAliasForId(id: string, ast: ts.Node) { +export function getTypeReferenceNameForId( + id: string, + ast: ts.Node +): string | null { const holes = findTypeholes(ast); const hole = holes.find( @@ -136,7 +166,7 @@ export function getTypeAliasForId(id: string, ast: ts.Node) { ); if (!hole) { - return; + return null; } const holeHasTypeVariable = @@ -145,8 +175,7 @@ export function getTypeAliasForId(id: string, ast: ts.Node) { hole.typeArguments.length > 0; if (holeHasTypeVariable) { - const typeReference = hole.typeArguments![0].getText(); - return findDeclarationWithName(typeReference, ast); + return hole.typeArguments![0].getText(); } const variableDeclaration = getWrappingVariableDeclaration(hole); @@ -159,11 +188,25 @@ export function getTypeAliasForId(id: string, ast: ts.Node) { const typeReference = ( variableDeclaration as ts.VariableDeclaration ).type!.getText(); - return findDeclarationWithName(typeReference, ast); + return typeReference; } - return null; } +export function getTypeAliasForId( + id: string, + ast: ts.Node +): + | ts.ImportDeclaration + | ts.TypeAliasDeclaration + | ts.InterfaceDeclaration + | null { + const name = getTypeReferenceNameForId(id, ast); + if (!name) { + return null; + } + + return findDeclarationWithName(name, ast); +} export function getWrappingVariableDeclaration( node: ts.Node From 13f7ec5e7e7d583d29afc7bbefd5c4e712a423e4 Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Tue, 18 May 2021 20:28:46 +0300 Subject: [PATCH 2/7] remove console log, add a check for resolved path from ts compiler --- packages/extension/src/listener.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/listener.ts b/packages/extension/src/listener.ts index 18e8fd6..d3def4b 100644 --- a/packages/extension/src/listener.ts +++ b/packages/extension/src/listener.ts @@ -139,15 +139,17 @@ async function onTypeExtracted(id: string, types: string) { projectRoot, relativePath, document.uri.path - )!; + ); + + if (!absolutePath) { + return error("TS Compiler couldn't resolve the import path"); + } try { document = await vscode.workspace.openTextDocument( vscode.Uri.file(absolutePath) ); } catch (err) { - console.log(err); - return error( "Failed to open the document the imported type is referring to", absolutePath, @@ -183,7 +185,6 @@ async function onTypeExtracted(id: string, types: string) { getEditorRange(typeAliasNode!.parent).start, typesToBeInserted ); - console.log(workEdits); vscode.workspace.applyEdit(workEdits); From 98de2073ca1fd2b1f3e679853721428a640b9667 Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Wed, 19 May 2021 18:27:26 +0300 Subject: [PATCH 3/7] fix performance issues --- .../extension/src/commands/addATypehole.ts | 28 +++++++++++++------ packages/extension/src/editor/utils.ts | 7 ++++- packages/extension/src/extension.ts | 21 ++------------ packages/extension/src/listener.ts | 8 +++++- packages/extension/src/parse/module.ts | 4 ++- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/extension/src/commands/addATypehole.ts b/packages/extension/src/commands/addATypehole.ts index 449294e..eb9759b 100644 --- a/packages/extension/src/commands/addATypehole.ts +++ b/packages/extension/src/commands/addATypehole.ts @@ -1,17 +1,27 @@ -import * as ts from 'typescript'; -import * as vscode from 'vscode'; +import * as ts from "typescript"; +import * as vscode from "vscode"; -import { getEditorRange } from '../editor/utils'; +import { getEditorRange } from "../editor/utils"; import { getPlaceholderTypeName, insertRecorderToSelection, insertTypeholeImport, last, startRenamingPlaceholderType, -} from '../extension'; -import { findTypeholes, getAST, getNodeEndPosition, getNodeStartPosition, getParentOnRootLevel } from '../parse/module'; -import { getNextAvailableId } from '../state'; -import { getWrappingVariableDeclaration, insertGenericTypeParameter, insertTypeReference } from '../transforms/insertTypes'; +} from "../extension"; +import { + findTypeholes, + getAST, + getNodeEndPosition, + getNodeStartPosition, + getParentOnRootLevel, +} from "../parse/module"; +import { getNextAvailableId } from "../state"; +import { + getWrappingVariableDeclaration, + insertGenericTypeParameter, + insertTypeReference, +} from "../transforms/insertTypes"; export async function addATypehole() { const editor = vscode.window.activeTextEditor; @@ -22,10 +32,12 @@ export async function addATypehole() { const fullFile = document.getText(); const ast = getAST(fullFile); + const id = getNextAvailableId(); await editor.edit((editBuilder) => { insertTypeholeImport(ast, editBuilder); + insertRecorderToSelection(id, editor, editBuilder); }); @@ -39,6 +51,7 @@ export async function addATypehole() { getWrappingVariableDeclaration(newlyCreatedTypeHole); const typeName = getPlaceholderTypeName(updatedAST); + await editor.edit((editBuilder) => { if (variableDeclaration && !variableDeclaration.type) { insertTypeToVariableDeclaration( @@ -121,4 +134,3 @@ function insertTypeToVariableDeclaration( ); } } - diff --git a/packages/extension/src/editor/utils.ts b/packages/extension/src/editor/utils.ts index ce629ff..4c310bb 100644 --- a/packages/extension/src/editor/utils.ts +++ b/packages/extension/src/editor/utils.ts @@ -28,7 +28,12 @@ export async function getPackageJSONDirectories() { "**/package.json" ); - const files = await vscode.workspace.findFiles(include); + const exclude = new vscode.RelativePattern( + getProjectURI()!, + "**/node_modules/**" + ); + + const files = await vscode.workspace.findFiles(include, exclude); // Done like this as findFiles didn't respect the exclude parameter return files.filter((f) => !f.path.includes("node_modules")); diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 2a605f6..a279827 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -45,29 +45,12 @@ export const last = (arr: T[]) => arr[arr.length - 1]; export function getPlaceholderTypeName(document: ts.SourceFile) { let n = 0; - let results = tsquery - .query(document, `TypeAliasDeclaration > Identifier[name="AutoDiscovered"]`) - .concat( - tsquery.query( - document, - `InterfaceDeclaration > Identifier[name="AutoDiscovered"]` - ) - ); + let results = tsquery.query(document, `Identifier[name="AutoDiscovered"]`); while (results.length > 0) { n++; - results = tsquery - .query( - document, - `TypeAliasDeclaration > Identifier[name="AutoDiscovered${n}"]` - ) - .concat( - tsquery.query( - document, - `InterfaceDeclaration > Identifier[name="AutoDiscovered${n}"]` - ) - ); + results = tsquery.query(document, `Identifier[name="AutoDiscovered${n}"]`); } return "AutoDiscovered" + (n === 0 ? "" : n); diff --git a/packages/extension/src/listener.ts b/packages/extension/src/listener.ts index d3def4b..e1ff5dd 100644 --- a/packages/extension/src/listener.ts +++ b/packages/extension/src/listener.ts @@ -107,6 +107,11 @@ async function onTypeExtracted(id: string, types: string) { ); if (!document) { + error( + "Document", + hole.fileName, + "a typehole was referring to was not found. This is not supposed to happen" + ); return; } @@ -120,13 +125,14 @@ async function onTypeExtracted(id: string, types: string) { const typeName = getTypeReferenceNameForId(id, ast)!; - const typeIsImportedFromAnotherFile = ts.isImportDeclaration(typeAliasNode); /* * Type is imported from another file */ + const typeIsImportedFromAnotherFile = ts.isImportDeclaration(typeAliasNode); if (typeIsImportedFromAnotherFile) { const relativePath = tsquery(typeAliasNode, "StringLiteral")[0] ?.getText() + // "./types.ts" -> types.ts .replace(/["']/g, ""); const projectRoot = await getProjectRoot(document); diff --git a/packages/extension/src/parse/module.ts b/packages/extension/src/parse/module.ts index dd406f1..7e843fe 100644 --- a/packages/extension/src/parse/module.ts +++ b/packages/extension/src/parse/module.ts @@ -27,7 +27,9 @@ export function resolveImportPath( const compilerOptions = ts.parseJsonConfigFileContent( configFile.config, ts.sys, - "./" + "./", + undefined, + configFileName ); function fileExists(fileName: string): boolean { From b6b15a439f700956409d5125e481735f1822791d Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Wed, 19 May 2021 19:57:12 +0300 Subject: [PATCH 4/7] fix type dependency finder not finding types inside a ParenthesizedType --- .../src/transforms/insertTypes/index.test.ts | 16 +++++++++++++++- .../src/transforms/insertTypes/index.ts | 8 +++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/transforms/insertTypes/index.test.ts b/packages/extension/src/transforms/insertTypes/index.test.ts index 5787030..472ba1d 100644 --- a/packages/extension/src/transforms/insertTypes/index.test.ts +++ b/packages/extension/src/transforms/insertTypes/index.test.ts @@ -1,6 +1,6 @@ import { tsquery } from "@phenomnomnominal/tsquery"; import { getAST } from "../../parse/module"; - +import * as ts from "typescript"; import { findAllDependencyTypeDeclarations, getAllDependencyTypeDeclarations, @@ -99,3 +99,17 @@ test("finds all dependency type declarations from an ast when there are array ty findAllDependencyTypeDeclarations(node.parent).map((n) => n.name.getText()) ).toEqual(["AutoDiscovered", "IRootObjectItem"]); }); + +test("finds dependent types from a ParenthesizedType", () => { + const ast = getAST(`export type Foo = (TypeholeRootWrapper | number); + interface TypeholeRootWrapper { + a: number; + } + `); + + const node = tsquery.query(ast, 'Identifier[name="Foo"]')[0]; + + expect( + findAllDependencyTypeDeclarations(node.parent).map((n) => n.name.getText()) + ).toEqual(["Foo", "TypeholeRootWrapper"]); +}); diff --git a/packages/extension/src/transforms/insertTypes/index.ts b/packages/extension/src/transforms/insertTypes/index.ts index afcec25..d4d3fea 100644 --- a/packages/extension/src/transforms/insertTypes/index.ts +++ b/packages/extension/src/transforms/insertTypes/index.ts @@ -19,10 +19,12 @@ function findDeclarationInImportedDeclarations( } function findDeclarationsWithName(name: string, ast: ts.Node) { - return tsquery.query( + const res = tsquery.query( ast, `:declaration > Identifier[name="${name}"]` ); + + return res; } export function findDeclarationWithName( @@ -140,6 +142,10 @@ export function findAllDependencyTypeDeclarations( ]; }); } + // (TypeholeRootWrapper | number) + if (ts.isParenthesizedTypeNode(node)) { + return findAllDependencyTypeDeclarations(node.type, [...found]); + } if ( ts.isArrayTypeNode(node) && ts.isParenthesizedTypeNode(node.elementType) From 0008874162e286bea44ec2a0a42c4523b9a1bfc1 Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Wed, 19 May 2021 19:58:52 +0300 Subject: [PATCH 5/7] change data structure to support the same hole identifier in multiple files this is to support a use case where you want to record to a same type from multiple places samples are dependant on typehole ids so you need to be using the same id for each --- .../commands/removeTypeholesFromAllFiles.ts | 6 +- packages/extension/src/extension.ts | 13 ++-- packages/extension/src/listener.ts | 17 +++-- packages/extension/src/parse/utils.ts | 5 ++ packages/extension/src/state.ts | 63 +++++++++++++------ 5 files changed, 71 insertions(+), 33 deletions(-) diff --git a/packages/extension/src/commands/removeTypeholesFromAllFiles.ts b/packages/extension/src/commands/removeTypeholesFromAllFiles.ts index 432da8a..e021962 100644 --- a/packages/extension/src/commands/removeTypeholesFromAllFiles.ts +++ b/packages/extension/src/commands/removeTypeholesFromAllFiles.ts @@ -1,11 +1,11 @@ import * as vscode from "vscode"; import { unique } from "../parse/utils"; -import { getState } from "../state"; +import { getAllHoles } from "../state"; import { removeTypeholesFromFile } from "./removeTypeholesFromCurrentFile"; export async function removeTypeholesFromAllFiles() { - const holes = getState().holes; - const files = holes.map((h) => h.fileName).filter(unique); + const holes = getAllHoles(); + const files = holes.flatMap((h) => h.fileNames).filter(unique); for (const file of files) { let document: null | vscode.TextDocument = null; diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index a279827..5df0f68 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -26,6 +26,7 @@ import { TypeHoler } from "./code-action"; import { clearWarnings, events, + getAllHoles, getState, onFileChanged, onFileDeleted, @@ -135,12 +136,12 @@ export async function activate(context: vscode.ExtensionContext) { let previousState = getState(); events.on("change", async (newState: State) => { - const allHolesRemoved = - previousState.holes.length > 0 && newState.holes.length === 0; + const previousHoles = Object.values(previousState.holes); + const newHoles = Object.values(newState.holes); + const allHolesRemoved = previousHoles.length > 0 && newHoles.length === 0; const shouldEnsureRuntime = - previousState.holes.length !== newState.holes.length && - newState.holes.length > 0; + previousHoles.length !== newHoles.length && newHoles.length > 0; previousState = newState; @@ -154,7 +155,7 @@ export async function activate(context: vscode.ExtensionContext) { await ensureRuntime(); } - if (newState.holes.length > 0 && !isServerRunning()) { + if (newHoles.length > 0 && !isServerRunning()) { try { vscode.window.showInformationMessage("Typehole: Starting server..."); await startListenerServer(); @@ -178,7 +179,7 @@ export async function activate(context: vscode.ExtensionContext) { ); await Promise.all(existingFiles.map(fileChanged)); - const holes = getState().holes; + const holes = getAllHoles(); log("Found", holes.length.toString(), "holes in the workspace"); /* diff --git a/packages/extension/src/listener.ts b/packages/extension/src/listener.ts index e1ff5dd..d5ae65f 100644 --- a/packages/extension/src/listener.ts +++ b/packages/extension/src/listener.ts @@ -1,6 +1,5 @@ import { tsquery } from "@phenomnomnominal/tsquery"; import f from "fastify"; -import { join } from "path"; import * as ts from "typescript"; import * as vscode from "vscode"; @@ -8,7 +7,7 @@ import * as vscode from "vscode"; import { getEditorRange, getProjectRoot } from "./editor/utils"; import { error, log } from "./logger"; import { findTypeholes, getAST, resolveImportPath } from "./parse/module"; -import { addSample, addWarning, getHole } from "./state"; +import { addSample, addWarning, getHole, Typehole } from "./state"; import { findDeclarationWithName, getAllDependencyTypeDeclarations, @@ -102,14 +101,20 @@ async function onTypeExtracted(id: string, types: string) { return; } + for (const fileName of hole.fileNames) { + await updateTypes(hole, types, fileName); + } +} + +async function updateTypes(hole: Typehole, types: string, fileName: string) { let document = await vscode.workspace.openTextDocument( - vscode.Uri.file(hole.fileName) + vscode.Uri.file(fileName) ); if (!document) { error( "Document", - hole.fileName, + fileName, "a typehole was referring to was not found. This is not supposed to happen" ); return; @@ -117,13 +122,13 @@ async function onTypeExtracted(id: string, types: string) { let ast = getAST(document.getText()); - let typeAliasNode = getTypeAliasForId(id, ast); + let typeAliasNode = getTypeAliasForId(hole.id, ast); if (!typeAliasNode) { return; } - const typeName = getTypeReferenceNameForId(id, ast)!; + const typeName = getTypeReferenceNameForId(hole.id, ast)!; /* * Type is imported from another file diff --git a/packages/extension/src/parse/utils.ts b/packages/extension/src/parse/utils.ts index 4d4b297..85c7da2 100644 --- a/packages/extension/src/parse/utils.ts +++ b/packages/extension/src/parse/utils.ts @@ -89,3 +89,8 @@ export function lineCharacterPositionInText( export function unique(value: T, index: number, self: T[]) { return self.indexOf(value) === index; } + +export function omit(original: T, key: keyof T) { + const { [key]: value, ...withoutKey } = original; + return withoutKey; +} diff --git a/packages/extension/src/state.ts b/packages/extension/src/state.ts index 4c63f16..407f6e9 100644 --- a/packages/extension/src/state.ts +++ b/packages/extension/src/state.ts @@ -3,15 +3,16 @@ import * as vscode from "vscode"; import { getId } from "./hole"; import { log } from "./logger"; import { findTypeholes, getAST } from "./parse/module"; +import { omit, unique } from "./parse/utils"; export const events = new EventEmitter(); -type TypeHole = { id: string; fileName: string }; +export type Typehole = { id: string; fileNames: string[] }; let state = { nextUniqueId: 0, warnings: {} as Record, - holes: [] as TypeHole[], + holes: {} as Record, samples: {} as Record, }; @@ -80,28 +81,50 @@ function clearSamples(id: string, currentState: typeof state) { } function createTypehole(id: string, fileName: string) { - const hole = { id, fileName }; + const existingHole = getHole(id); + const hole = existingHole + ? { id, fileNames: existingHole.fileNames.concat(fileName).filter(unique) } + : { id, fileNames: [fileName] }; const currentState = getState(); setState({ ...currentState, nextUniqueId: currentState.nextUniqueId + 1, - holes: [...currentState.holes, hole], + holes: { ...currentState.holes, [id]: hole }, }); } -function removeTypehole(id: string) { +function removeTypeholeFromFile(id: string, fileName: string) { const currentState = getState(); - setState( - clearSamples(id, { - ...currentState, - holes: currentState.holes.filter((h) => h.id !== id), - }) + const hole = getHole(id); + if (!hole) { + return; + } + const fileFilesWithoutFile = hole?.fileNames.filter( + (file) => file !== fileName ); + const wasOnlyFileWithTypehole = fileFilesWithoutFile.length === 0; + + if (wasOnlyFileWithTypehole) { + const newHoles = omit(currentState.holes, id); + setState( + clearSamples(id, { + ...currentState, + holes: newHoles, + }) + ); + } else { + const holeWithoutFile = { ...hole, fileNames: fileFilesWithoutFile }; + setState({ + ...currentState, + holes: { ...currentState.holes, [id]: holeWithoutFile }, + }); + } } function setState(newState: typeof state): void { state = newState; + events.emit("change", newState); } @@ -109,15 +132,19 @@ export function getState() { return state; } +export function getAllHoles() { + return Object.values(getState().holes); +} + export function onFileDeleted(fileName: string) { - getState() - .holes.filter((hole) => hole.fileName === fileName) - .forEach((h) => removeTypehole(h.id)); + getAllHoles() + .filter((hole) => hole.fileNames.includes(fileName)) + .forEach((h) => removeTypeholeFromFile(h.id, fileName)); } export function onFileChanged(fileName: string, content: string) { - const knownHolesInThisFile = state.holes.filter( - (hole) => hole.fileName === fileName + const knownHolesInThisFile = getAllHoles().filter((hole) => + hole.fileNames.includes(fileName) ); const knownIds = knownHolesInThisFile.map(({ id }) => id); @@ -135,11 +162,11 @@ export function onFileChanged(fileName: string, content: string) { knownIds.forEach((holeId) => { const holeHasBeenRemoved = !holesInDocument.includes(holeId); if (holeHasBeenRemoved) { - removeTypehole(holeId); + removeTypeholeFromFile(holeId, fileName); } }); } -export function getHole(id: string) { - return state.holes.find((hole) => hole.id === id); +export function getHole(id: string): Typehole | undefined { + return state.holes[id]; } From 8e59589f34fa03665ed73fdb8a94ac18eeb256ae Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Wed, 19 May 2021 19:59:45 +0300 Subject: [PATCH 6/7] remove debouncing from sample emitter to support having the same typehole id in multiple places, emitting values almost at the same time --- packages/runtime/package-lock.json | 10 ++++++++-- packages/runtime/package.json | 2 +- packages/runtime/src/index.ts | 18 +----------------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/runtime/package-lock.json b/packages/runtime/package-lock.json index 53888e7..844e04c 100644 --- a/packages/runtime/package-lock.json +++ b/packages/runtime/package-lock.json @@ -1,12 +1,12 @@ { "name": "typehole", - "version": "1.2.1", + "version": "1.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "typehole", - "version": "1.2.1", + "version": "1.5.1", "license": "MIT", "dependencies": { "@types/isomorphic-fetch": "0.0.35", @@ -365,6 +365,9 @@ "version": "4.0.0", "dev": true, "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -519,6 +522,9 @@ "version": "2.21.0", "dev": true, "license": "MIT", + "dependencies": { + "fsevents": "~2.1.2" + }, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 12fd4c1..4f9e818 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,5 +1,5 @@ { - "version": "1.2.1", + "version": "1.5.1", "name": "typehole", "repository": "rikukissa/typehole", "description": "Turn runtime types into static typescript types automatically", diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index cccf6cd..ec5af43 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -46,22 +46,6 @@ function sendSample(holeId: HoleId, input: any) { return sampleQueue; } -const debounce = any>( - func: F, - waitFor: number -) => { - let timeout: NodeJS.Timeout; - - return (...args: Parameters): Promise> => - new Promise((resolve) => { - if (timeout) { - clearTimeout(timeout); - } - - timeout = setTimeout(() => resolve(func(...args)), waitFor); - }); -}; - async function solveWrapperTypes(value: any) { if (typeof value?.then === "function") { return { @@ -73,7 +57,7 @@ async function solveWrapperTypes(value: any) { } function typeholeFactory(id: HoleId) { - const emitSample = debounce(sendSample, 300); + const emitSample = sendSample; let previousValue: string | null = null; return function typehole(input: T): T { From 6d036fe30f97de8c16bef1816779acf5de823904 Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Wed, 19 May 2021 20:07:50 +0300 Subject: [PATCH 7/7] update release notes --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a3ff765..64177f1 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,8 @@ From 1.4.0 forward also Promises are supported. All other values (functions etc. ### Fixed +- 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. +- No duplicated interfaces anymore when the generated top-level type is a `ParenthesizedType` - Interface not updating when it was in a different file than the typehole - Types not updating when some other file was focused in the editor - `typehole.tNaN` [issue](https://github.com/rikukissa/typehole/issues/7) when there have been typeholes with a non `t` format