diff --git a/package-lock.json b/package-lock.json index 6fdd9827..26da4386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "open": "^8.4.2", "postman-request": "^2.88.1-postman.32", "pretty-bytes": "^5.6.0", + "resolve": "^1.22.8", "roku-debug": "^0.21.7", "roku-deploy": "^3.12.0", "roku-test-automation": "^2.0.6", @@ -58,6 +59,7 @@ "@types/node": "^12.12.0", "@types/node-ssdp": "^3.3.0", "@types/prompt": "^1.1.2", + "@types/resolve": "^1.20.6", "@types/semver": "^7.1.0", "@types/sinon": "7.0.6", "@types/vscode": "^1.53.0", @@ -1503,6 +1505,12 @@ "form-data": "^2.5.0" } }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true + }, "node_modules/@types/revalidator": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@types/revalidator/-/revalidator-0.3.8.tgz", @@ -5167,9 +5175,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -5670,6 +5681,17 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -6067,12 +6089,11 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8084,8 +8105,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { "version": "0.1.7", @@ -8777,12 +8797,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -9937,7 +9956,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, diff --git a/package.json b/package.json index d706954b..66bea7c4 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "open": "^8.4.2", "postman-request": "^2.88.1-postman.32", "pretty-bytes": "^5.6.0", + "resolve": "^1.22.8", "roku-debug": "^0.21.7", "roku-deploy": "^3.12.0", "roku-test-automation": "^2.0.6", @@ -100,6 +101,7 @@ "@types/node": "^12.12.0", "@types/node-ssdp": "^3.3.0", "@types/prompt": "^1.1.2", + "@types/resolve": "^1.20.6", "@types/semver": "^7.1.0", "@types/sinon": "7.0.6", "@types/vscode": "^1.53.0", diff --git a/src/LanguageServerManager.ts b/src/LanguageServerManager.ts index ac150bb0..17e186a6 100644 --- a/src/LanguageServerManager.ts +++ b/src/LanguageServerManager.ts @@ -321,12 +321,12 @@ export class LanguageServerManager { this.selectedBscInfo = { path: bsdkPath, // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires - version: require(`${bsdkPath}/package.json`).version + version: fsExtra.readJsonSync(`${bsdkPath}/package.json`).version }; } catch (e) { console.error(e); //fall back to the embedded version, and show a popup - await vscode.window.showErrorMessage(`Can't find language server at "${bsdkPath}". Using embedded version v${this.embeddedBscInfo.version} instead.`); + await vscode.window.showErrorMessage(`Can't find language server at "${bsdkPath}". Did you forget to run \`npm install\`? Using embedded version v${this.embeddedBscInfo.version} instead.`); this.selectedBscInfo = this.embeddedBscInfo; } diff --git a/src/commands/LanguageServerInfoCommand.spec.ts b/src/commands/LanguageServerInfoCommand.spec.ts new file mode 100644 index 00000000..4500a7c3 --- /dev/null +++ b/src/commands/LanguageServerInfoCommand.spec.ts @@ -0,0 +1,144 @@ +import * as fsExtra from 'fs-extra'; +import { standardizePath as s } from 'brighterscript'; +import { LanguageServerInfoCommand } from './LanguageServerInfoCommand'; +import { expect } from 'chai'; +import { createSandbox } from 'sinon'; +import * as resolve from 'resolve'; + +const sinon = createSandbox(); + +const cwd = s`${__dirname}../../../`; +const tempDir = s`${cwd}/.tmp`; +const embeddedBscVersion = require('brighterscript/package.json').version; + +describe('LanguageServerInfoCommand', () => { + let command: LanguageServerInfoCommand; + beforeEach(() => { + sinon.restore(); + fsExtra.ensureDirSync(tempDir); + command = new LanguageServerInfoCommand(); + const orig = resolve.sync.bind(resolve) as typeof resolve['sync']; + sinon.stub(resolve, 'sync').callsFake((moduleName: string, options?: any) => { + return orig(moduleName, { + ...options ?? {}, + packageIterator: (request, start, getPackageCandidates, opts) => { + const candidates = getPackageCandidates(); + const filtered = candidates.filter(candidate => s(candidate).startsWith(tempDir)); + return filtered; + } + }); + }); + }); + + afterEach(() => { + sinon.restore(); + fsExtra.removeSync(tempDir); + }); + + describe('discoverBrighterScriptVersions', () => { + function writePackage(version: string) { + fsExtra.outputJsonSync(s`${tempDir}/package.json`, { + name: 'vscode-tests', + private: true, + dependencies: { + brighterscript: version + } + }); + fsExtra.outputFileSync(s`${tempDir}/node_modules/brighterscript/dist/index.js`, ''); + fsExtra.outputJsonSync(s`${tempDir}/node_modules/brighterscript/package.json`, { + name: 'brighterscript', + version: version, + main: 'dist/index.js', + dependencies: {} + }); + } + it('finds embedded version when node_modules is not present', () => { + expect( + command['discoverBrighterScriptVersions']([tempDir]) + ).to.eql([{ + label: `Use VSCode's version`, + description: embeddedBscVersion + }]); + }); + + it('finds embedded version when node_modules is present but brighterscript is not available', () => { + fsExtra.outputJsonSync(s`${tempDir}/package.json`, { + name: 'vscode-tests', + private: true, + dependencies: {} + }); + fsExtra.outputJsonSync(s`${tempDir}/node_modules/is-number/package.json`, { + name: 'is-number', + dependencies: {} + }); + expect( + command['discoverBrighterScriptVersions']([tempDir]) + ).to.eql([{ + label: `Use VSCode's version`, + description: embeddedBscVersion + }]); + }); + + it('finds brighterscript version from node_modules', () => { + writePackage('1.2.3'); + expect( + command['discoverBrighterScriptVersions']([tempDir]) + ).to.eql([{ + label: `Use VSCode's version`, + description: embeddedBscVersion + }, { + label: `Use Workspace Version`, + description: '1.2.3', + detail: 'node_modules/brighterscript' + }]); + }); + + it('does not cache brighterscript version from node_modules in subsequent calls', () => { + writePackage('1.2.3'); + expect( + command['discoverBrighterScriptVersions']([tempDir]) + ).to.eql([{ + label: `Use VSCode's version`, + description: embeddedBscVersion + }, { + label: `Use Workspace Version`, + description: '1.2.3', + detail: 'node_modules/brighterscript' + }]); + + writePackage('2.3.4'); + expect( + command['discoverBrighterScriptVersions']([tempDir]) + ).to.eql([{ + label: `Use VSCode's version`, + description: embeddedBscVersion + }, { + label: `Use Workspace Version`, + description: '2.3.4', + detail: 'node_modules/brighterscript' + }]); + }); + + it('excludes value when module is deleted since last time', () => { + writePackage('1.2.3'); + expect( + command['discoverBrighterScriptVersions']([tempDir]) + ).to.eql([{ + label: `Use VSCode's version`, + description: embeddedBscVersion + }, { + label: `Use Workspace Version`, + description: '1.2.3', + detail: 'node_modules/brighterscript' + }]); + + fsExtra.removeSync(`${tempDir}/node_modules`); + expect( + command['discoverBrighterScriptVersions']([tempDir]) + ).to.eql([{ + label: `Use VSCode's version`, + description: embeddedBscVersion + }]); + }); + }); +}); diff --git a/src/commands/LanguageServerInfoCommand.ts b/src/commands/LanguageServerInfoCommand.ts index 7f8d7b01..cdd8e5e3 100644 --- a/src/commands/LanguageServerInfoCommand.ts +++ b/src/commands/LanguageServerInfoCommand.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode'; import { LANGUAGE_SERVER_NAME, languageServerManager } from '../LanguageServerManager'; import * as path from 'path'; +import * as resolve from 'resolve'; +import * as fsExtra from 'fs-extra'; export class LanguageServerInfoCommand { public static commandName = 'extension.brightscript.languageServer.info'; @@ -38,50 +40,63 @@ export class LanguageServerInfoCommand { await vscode.commands.executeCommand('extension.brightscript.languageServer.restart'); } - /** - * If this changes the user/folder/workspace settings, that will trigger a reload of the language server so there's no need to - * call the reload manually - */ - public async selectBrighterScriptVersion() { - const versions = [{ - label: `Use VS Code's version`, - description: languageServerManager.embeddedBscInfo.version, - detail: undefined as string //require.resolve('brighterscript') + private discoverBrighterScriptVersions(workspaceFolders: string[]): BscVersionInfo[] { + const versions: BscVersionInfo[] = [{ + label: `Use VSCode's version`, + description: languageServerManager.embeddedBscInfo.version }]; - //look for brighterscript in all workspace folders - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - const workspaceOrFolderPath = this.getWorkspaceOrFolderPath(workspaceFolder.uri.fsPath); + //look for brighterscript in node_modules from all workspace folders + for (const workspaceFolder of workspaceFolders) { + let bscPath: string; try { - let bscPath = require.resolve('brighterscript', { - paths: [workspaceFolder.uri.fsPath] + + bscPath = resolve.sync('brighterscript', { + basedir: workspaceFolder }); - //require.resolve returns a bsc script path, so remove that to get the root of brighterscript folder - if (bscPath) { - bscPath = bscPath.replace(/[\\\/]dist[\\\/]index.js/i, ''); - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires - const version = require(`${bscPath}/package.json`).version; - //make the path relative to the workspace folder - bscPath = path.relative(workspaceOrFolderPath, bscPath); + } catch (e) { + //could not resolve the path, so just move on + } + + //resolve returns a bsc script path, so remove that to get the root of brighterscript folder + if (bscPath) { + bscPath = bscPath.replace(/[\\\/]dist[\\\/]index.js/i, ''); + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const version = fsExtra.readJsonSync(`${bscPath}/package.json`).version; + //make the path relative to the workspace folder + bscPath = path.relative(workspaceFolder, bscPath); - versions.push({ - label: 'Use Workspace Version', - description: version, - detail: bscPath - }); - } - } finally { } + versions.push({ + label: 'Use Workspace Version', + description: version, + detail: bscPath.replace(/\\+/g, '/') + }); + } } + return versions; + } + + /** + * If this changes the user/folder/workspace settings, that will trigger a reload of the language server so there's no need to + * call the reload manually + */ + public async selectBrighterScriptVersion() { + const versions = this.discoverBrighterScriptVersions( + vscode.workspace.workspaceFolders.map(x => this.getWorkspaceOrFolderPath(x.uri.fsPath)) + ); let selection = await vscode.window.showQuickPick(versions, { placeHolder: `Select the BrighterScript version used for BrightScript and BrighterScript language features` }); if (selection) { const config = vscode.workspace.getConfiguration('brightscript'); + //quickly clear the setting, then set it again so we are guaranteed to trigger a change event + await config.update('bsdk', undefined); + //if the user picked "use embedded version", then remove the setting if (versions.indexOf(selection) === 0) { //setting to undefined means "remove" await config.update('bsdk', 'embedded'); return 'embedded'; } else { - //save this to workspace/folder settings (vscode automatically decides if it goes into the code-workspace settings or the folder settings + //save this to workspace/folder settings (vscode automatically decides if it goes into the code-workspace settings or the folder settings) await config.update('bsdk', selection.detail); return selection.detail; } @@ -98,4 +113,10 @@ export class LanguageServerInfoCommand { } } +interface BscVersionInfo { + label: string; + description: string; + detail?: string; +} + export const languageServerInfoCommand = new LanguageServerInfoCommand();