From 7fcf17e097b9dc7223434a93cee840d9af3cc2b4 Mon Sep 17 00:00:00 2001 From: YuTengjing Date: Tue, 2 Apr 2024 22:27:43 +0800 Subject: [PATCH] refactor: encapsulation parseJsonc --- src/codeLens/packageJsonDependencies.ts | 10 ++----- src/codeLens/packageJsonFiles.ts | 18 ++++--------- src/codeLens/packageJsonVersion.ts | 12 +++------ src/codeLens/pnpmWorkspace.ts | 2 +- src/commands/addMissingDeps.ts | 13 +++++---- src/definitions/dependencies.ts | 6 ++--- src/hoverTooltips/dependencies.ts | 8 ++---- src/hoverTooltips/npmScripts.ts | 11 +++----- src/utils/editor.ts | 10 +------ src/utils/jsonc.ts | 21 +++++++++++++++ src/utils/pkg-info.ts | 1 + src/utils/pkg.ts | 36 +++++++++++-------------- 12 files changed, 65 insertions(+), 83 deletions(-) create mode 100644 src/utils/jsonc.ts diff --git a/src/codeLens/packageJsonDependencies.ts b/src/codeLens/packageJsonDependencies.ts index 5401e22..43b117f 100644 --- a/src/codeLens/packageJsonDependencies.ts +++ b/src/codeLens/packageJsonDependencies.ts @@ -6,7 +6,7 @@ import { CodeLens, workspace } from 'vscode'; import { configuration, configurationKeys } from '../configuration'; import { commands } from '../utils/constants'; -import { jsoncStringNodeToRange } from '../utils/editor'; +import { jsoncStringNodeToRange, parseJsonc } from '../utils/jsonc'; import type { SearchImportsMatch } from '../utils/searchImports'; import { BaseCodeLensProvider } from './BaseCodeLensProvider'; @@ -95,13 +95,7 @@ export class PackageJsonDependenciesCodeLensProvider extends BaseCodeLensProvide _token: CancellationToken, ): Promise { const packageJson = document.getText(); - const { parseTree } = await import('jsonc-parser'); - let root: Node | undefined; - try { - root = parseTree(packageJson); - } catch { - return; - } + const root = await parseJsonc(packageJson); if (!root) return; const dependencies: Dependency[] = ( diff --git a/src/codeLens/packageJsonFiles.ts b/src/codeLens/packageJsonFiles.ts index 58f0c6b..fbd8880 100644 --- a/src/codeLens/packageJsonFiles.ts +++ b/src/codeLens/packageJsonFiles.ts @@ -1,12 +1,11 @@ import { dirname, resolve } from 'node:path'; -import type { Node } from 'jsonc-parser'; import type { CancellationToken, ExtensionContext, TextDocument } from 'vscode'; import { CodeLens, Range } from 'vscode'; import { configuration, configurationKeys } from '../configuration'; -import { jsoncStringNodeToRange } from '../utils/editor'; import { pathExists } from '../utils/fs'; +import { jsoncStringNodeToRange, parseJsonc } from '../utils/jsonc'; import { GlobCodeLensProvider } from './GlobCodeLensProvider'; const filesLiteral = 'files'; @@ -53,19 +52,12 @@ export class PackageJsonFilesCodeLensProvider extends GlobCodeLensProvider { super.getCodeLenses(document, _token); const { globby } = await import('globby'); - const { parseTree, findNodeAtLocation } = await import('jsonc-parser'); + const { findNodeAtLocation } = await import('jsonc-parser'); - const filePath = document.uri.fsPath; const packageJson = document.getText(); - let root: Node | undefined; - try { - // jsonc has builtin cache - root = parseTree(packageJson); - } catch { - return; - } - + const root = await parseJsonc(packageJson); if (!root) return; + const filesPropertyNode = findNodeAtLocation(root, ['files']); if ( !filesPropertyNode || @@ -98,7 +90,7 @@ export class PackageJsonFilesCodeLensProvider extends GlobCodeLensProvider { const totalFiles: Set = new Set(); const codeLensList: CodeLens[] = []; const promises: Array> = []; - const cwd = dirname(filePath); + const cwd = dirname(document.uri.fsPath); for (const item of patternList) { const codeLens = new CodeLens(item.range); codeLensList.push(codeLens); diff --git a/src/codeLens/packageJsonVersion.ts b/src/codeLens/packageJsonVersion.ts index 4c0defa..b1961ff 100644 --- a/src/codeLens/packageJsonVersion.ts +++ b/src/codeLens/packageJsonVersion.ts @@ -1,5 +1,4 @@ import ExpiryMap from 'expiry-map'; -import type { Node } from 'jsonc-parser'; import pMemoize from 'p-memoize'; import fetchPackageJson from 'package-json'; import type { CancellationToken, ExtensionContext, Range, TextDocument } from 'vscode'; @@ -7,7 +6,7 @@ import { CodeLens } from 'vscode'; import { configuration, configurationKeys } from '../configuration'; import { commands } from '../utils/constants'; -import { jsoncStringNodeToRange } from '../utils/editor'; +import { jsoncStringNodeToRange, parseJsonc } from '../utils/jsonc'; import { BaseCodeLensProvider } from './BaseCodeLensProvider'; interface CodeLensData { @@ -57,13 +56,8 @@ export class PackageJsonVersionCodeLensProvider extends BaseCodeLensProvider { _token: CancellationToken, ): Promise { const packageJson = document.getText(); - const { parseTree, findNodeAtLocation } = await import('jsonc-parser'); - let root: Node | undefined; - try { - root = parseTree(packageJson); - } catch { - return; - } + const { findNodeAtLocation } = await import('jsonc-parser'); + const root = await parseJsonc(packageJson); if (!root) return; const codeLensList: CodeLens[] = []; diff --git a/src/codeLens/pnpmWorkspace.ts b/src/codeLens/pnpmWorkspace.ts index eeb3b5d..6df8982 100644 --- a/src/codeLens/pnpmWorkspace.ts +++ b/src/codeLens/pnpmWorkspace.ts @@ -4,7 +4,7 @@ import type { CancellationToken, ExtensionContext, TextDocument } from 'vscode'; import { CodeLens, Range } from 'vscode'; import { configuration, configurationKeys } from '../configuration'; -import { jsoncStringNodeToRange } from '../utils/editor'; +import { jsoncStringNodeToRange } from '../utils/jsonc'; import { GlobCodeLensProvider } from './GlobCodeLensProvider'; const packagesLiteral = 'packages'; diff --git a/src/commands/addMissingDeps.ts b/src/commands/addMissingDeps.ts index 9a33155..67711f3 100644 --- a/src/commands/addMissingDeps.ts +++ b/src/commands/addMissingDeps.ts @@ -1,13 +1,14 @@ import { dirname } from 'node:path'; -import * as jsonc from 'jsonc-parser'; import type { TextEditor } from 'vscode'; import { Range } from 'vscode'; +import { parseJsonc } from '../utils/jsonc'; import { getPackageInfoFromNpmView } from '../utils/pkg'; import { searchUsedDeps } from '../utils/searchUsedDeps'; export async function addMissingDeps(editor: TextEditor) { + const { findNodeAtLocation, getNodeValue, modify } = await import('jsonc-parser'); const { document } = editor; const pkgJsonPath = document.fileName; @@ -15,10 +16,12 @@ export async function addMissingDeps(editor: TextEditor) { const missingDeps = await searchUsedDeps(cwd); const pkgJson = document.getText(); - const root = jsonc.parseTree(pkgJson)!; + const root = await parseJsonc(pkgJson)!; + if (!root) return; + const dependenciesNodePath = ['dependencies']; - const dependenciesNode = jsonc.findNodeAtLocation(root, dependenciesNodePath); - const dependencies = dependenciesNode ? jsonc.getNodeValue(dependenciesNode) : {}; + const dependenciesNode = findNodeAtLocation(root, dependenciesNodePath); + const dependencies = dependenciesNode ? getNodeValue(dependenciesNode) : {}; const missingDepsObj = missingDeps.reduce( (obj, packageName) => { if (packageName in dependencies) return obj; @@ -50,7 +53,7 @@ export async function addMissingDeps(editor: TextEditor) { await Promise.all(getVersionPromises); // keep origin json format - const edits = jsonc.modify(pkgJson, dependenciesNodePath, newDependencies, { + const edits = modify(pkgJson, dependenciesNodePath, newDependencies, { formattingOptions: { tabSize: editor.options.tabSize as number, }, diff --git a/src/definitions/dependencies.ts b/src/definitions/dependencies.ts index ebd3711..1123604 100644 --- a/src/definitions/dependencies.ts +++ b/src/definitions/dependencies.ts @@ -10,7 +10,8 @@ import { Uri, } from 'vscode'; -import { getFileRange, jsoncStringNodeToRange } from '../utils/editor'; +import { getFileRange } from '../utils/editor'; +import { jsoncStringNodeToRange } from '../utils/jsonc'; import { findPackagePath, getPkgNameAndVersionFromDocPosition } from '../utils/pkg'; export class DependenciesDefinitionProvider implements DefinitionProvider { @@ -22,8 +23,7 @@ export class DependenciesDefinitionProvider implements DefinitionProvider { const pkgInfo = await getPkgNameAndVersionFromDocPosition(document, position); if (!pkgInfo) return; - const pkgJsonPath = await fs.realpath(document.uri.fsPath); - const pkgPath = await findPackagePath(pkgInfo.name, pkgJsonPath); + const pkgPath = await findPackagePath(pkgInfo.name, document.uri.fsPath); if (!pkgPath) return; const [targetUri, targetRange] = await Promise.all([ diff --git a/src/hoverTooltips/dependencies.ts b/src/hoverTooltips/dependencies.ts index 31d8b83..f6d2be4 100644 --- a/src/hoverTooltips/dependencies.ts +++ b/src/hoverTooltips/dependencies.ts @@ -1,5 +1,3 @@ -import fs from 'node:fs/promises'; - import type { CancellationToken, HoverProvider, Position, TextDocument } from 'vscode'; import { Hover } from 'vscode'; @@ -14,17 +12,15 @@ export class DependenciesHoverProvider implements HoverProvider { token: CancellationToken, ): Promise { const pkgNameAndVersion = await getPkgNameAndVersionFromDocPosition(document, position); - if (!pkgNameAndVersion) return; const { name, version } = pkgNameAndVersion; - const pkgJsonPath = await fs.realpath(document.uri.fsPath); const info = await getPackageInfo(name, { - packageInstalledPath: (await findPackagePath(name, pkgJsonPath))?.pkgDir, + packageInstalledPath: (await findPackagePath(name, document.uri.fsPath))?.pkgDir, searchVersionRange: version, fetchBundleSize: true, remoteFetch: true, - // Note: 当 package.json 中定义了依赖类似"path"这样与 node 内置模块相同名称的包时,永远认为他不是使用 node 内置模块 + // Note: 当 package.json 中定义了依赖类似 "path" 这样与 node 内置模块相同名称的包时,永远认为他不是使用 node 内置模块 skipBuiltinModuleCheck: true, token, }); diff --git a/src/hoverTooltips/npmScripts.ts b/src/hoverTooltips/npmScripts.ts index c8c8a80..4831926 100644 --- a/src/hoverTooltips/npmScripts.ts +++ b/src/hoverTooltips/npmScripts.ts @@ -1,10 +1,10 @@ import { dirname } from 'node:path'; -import type { Node } from 'jsonc-parser'; import type { CancellationToken, HoverProvider, Position, TextDocument } from 'vscode'; import { Hover, MarkdownString, Range } from 'vscode'; import { commands } from '../utils/constants'; +import { parseJsonc } from '../utils/jsonc'; export class NpmScriptsHoverProvider implements HoverProvider { async provideHover( @@ -15,13 +15,8 @@ export class NpmScriptsHoverProvider implements HoverProvider { const filePath = document.uri.fsPath; const packageJson = document.getText(); - const { parseTree, findNodeAtOffset, findNodeAtLocation } = await import('jsonc-parser'); - let root: Node | undefined; - try { - root = parseTree(packageJson); - } catch { - return; - } + const { findNodeAtOffset, findNodeAtLocation } = await import('jsonc-parser'); + const root = await parseJsonc(packageJson); if (!root) return; const scriptNameNode = findNodeAtOffset(root, document.offsetAt(position)); diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 5faa2f0..a33413c 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -1,7 +1,6 @@ import { readFile } from 'node:fs/promises'; -import type { Node } from 'jsonc-parser'; -import type { Location, TextDocument, Uri } from 'vscode'; +import type { Location, Uri } from 'vscode'; import vscode, { Position, Range } from 'vscode'; export function goToLocation(uri: Uri, position: Position, location: Location) { @@ -16,13 +15,6 @@ export function goToLocation(uri: Uri, position: Position, location: Location) { ); } -export function jsoncStringNodeToRange(document: TextDocument, node: Node): Range { - return new Range( - document.positionAt(node.offset + 1), - document.positionAt(node.offset + node.length - 1), - ); -} - export async function getFileRange(filePath: string) { const textContent = await readFile(filePath, 'utf8'); const lines = textContent.split(/\r?\n/); diff --git a/src/utils/jsonc.ts b/src/utils/jsonc.ts new file mode 100644 index 0000000..466165c --- /dev/null +++ b/src/utils/jsonc.ts @@ -0,0 +1,21 @@ +import type { Node } from 'jsonc-parser'; +import type { TextDocument } from 'vscode'; +import { Range } from 'vscode'; + +export async function parseJsonc(json: string) { + const jsoncParser = await import('jsonc-parser'); + let root: Node | undefined; + try { + root = jsoncParser.parseTree(json); + } catch { + return; + } + return root; +} + +export function jsoncStringNodeToRange(document: TextDocument, node: Node): Range { + return new Range( + document.positionAt(node.offset + 1), + document.positionAt(node.offset + node.length - 1), + ); +} diff --git a/src/utils/pkg-info.ts b/src/utils/pkg-info.ts index 9d3130b..bb6eac7 100644 --- a/src/utils/pkg-info.ts +++ b/src/utils/pkg-info.ts @@ -42,6 +42,7 @@ const fetchPackageJson = promiseDebounce(_fetchPackageJson, (pkgNameAndRangeVers }); const remotePkgMetadataCache = new LRUCache({ max: 100, + // 10 mins ttl: 1000 * 60 * 10, }); async function getRemotePackageJsonData(pkgName: string, pkgVersion?: string) { diff --git a/src/utils/pkg.ts b/src/utils/pkg.ts index 6c7f497..b4b49cd 100644 --- a/src/utils/pkg.ts +++ b/src/utils/pkg.ts @@ -1,32 +1,32 @@ +import fs from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { execa } from 'execa'; -import type { Node } from 'jsonc-parser'; import type { JsonValue } from 'type-fest'; import validatePkgName from 'validate-npm-package-name'; import type { Position, TextDocument } from 'vscode'; -import { NODE_MODULES } from './constants'; -import { isFile, pathExists } from './fs'; +import { NODE_MODULES, PACKAGE_JSON } from './constants'; +import { pathExists } from './fs'; +import { parseJsonc } from './jsonc'; import { getRootOfPath, trimRightSlash } from './path'; -export async function findPackagePath(packageName: string, startPath: string, endPath?: string) { +export async function findPackagePath(packageName: string, baseFilePath: string, endPath?: string) { if (endPath === undefined) { - endPath = await getRootOfPath(startPath); + endPath = await getRootOfPath(baseFilePath); } - startPath = trimRightSlash(startPath); + // maybe soft symbolic link + baseFilePath = await fs.realpath(baseFilePath); + endPath = trimRightSlash(endPath); - if (await isFile(startPath)) { - startPath = dirname(startPath); - } - let currentDirPath = startPath; + let currentDirPath = dirname(baseFilePath); let end = false; let pkgDir = ''; do { pkgDir = resolve(currentDirPath, NODE_MODULES, packageName); - const pkgJsonPath = resolve(pkgDir, 'package.json'); + const pkgJsonPath = resolve(pkgDir, PACKAGE_JSON); // eslint-disable-next-line no-await-in-loop if ((await Promise.all([pathExists(pkgDir), pathExists(pkgJsonPath)])).every(Boolean)) { return { @@ -45,15 +45,8 @@ export async function getPkgNameAndVersionFromDocPosition( document: TextDocument, position: Position, ) { - const jsoncParser = await import('jsonc-parser'); - const pkgJson = document.getText(); - let root: Node | undefined; - try { - root = jsoncParser.parseTree(pkgJson); - } catch { - return; - } + const root = await parseJsonc(pkgJson); if (!root) return; const dependenciesNodePath = [ @@ -63,11 +56,12 @@ export async function getPkgNameAndVersionFromDocPosition( 'optionalDependencies', ]; - const nameNode = jsoncParser.findNodeAtOffset(root, document.offsetAt(position)); + const { findNodeAtOffset, findNodeAtLocation } = await import('jsonc-parser'); + const nameNode = findNodeAtOffset(root, document.offsetAt(position)); if (!nameNode || !nameNode.parent) return; const dependenciesNodes = dependenciesNodePath.map((path) => - jsoncParser.findNodeAtLocation(root, path.split('.')), + findNodeAtLocation(root, path.split('.')), ); const versionNode = nameNode.parent.children![1];