diff --git a/extensions/data-workspace/package.json b/extensions/data-workspace/package.json index c47159118b..9ec01d2a12 100644 --- a/extensions/data-workspace/package.json +++ b/extensions/data-workspace/package.json @@ -34,6 +34,11 @@ "projects.defaultProjectSaveLocation": { "type": "string", "description": "%projects.defaultProjectSaveLocation%" + }, + "projects.showNotAddedProjectsInWorkspacePrompt": { + "type": "boolean", + "description": "%projects.showNotAddedProjectsInWorkspacePrompt%", + "default": true } } } @@ -153,6 +158,7 @@ ] }, "dependencies": { + "fast-glob": "^3.1.0", "@microsoft/ads-extension-telemetry": "^1.1.1", "vscode-nls": "^4.0.0" }, diff --git a/extensions/data-workspace/package.nls.json b/extensions/data-workspace/package.nls.json index 333ae5b6ce..930c62a15e 100644 --- a/extensions/data-workspace/package.nls.json +++ b/extensions/data-workspace/package.nls.json @@ -10,5 +10,6 @@ "projects-view-no-workspace-content": "No workspace open, create new or open existing to get started.\n[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\nTo learn more about SQL Database projects [read our docs](https://aka.ms/azuredatastudio-sqlprojects).", "projects-view-no-project-content": "No projects found in current workspace.\n[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\nTo learn more about SQL Database projects [read our docs](https://aka.ms/azuredatastudio-sqlprojects).\n", "open-existing-command": "Open existing", - "projects.defaultProjectSaveLocation": "Full path to folder where new projects are saved by default." + "projects.defaultProjectSaveLocation": "Full path to folder where new projects are saved by default.", + "projects.showNotAddedProjectsInWorkspacePrompt": "Always show information message when the current workspace folders contain projects that have not been added to the workspace's projects." } diff --git a/extensions/data-workspace/src/common/constants.ts b/extensions/data-workspace/src/common/constants.ts index 00ce00eb15..5b65300e61 100644 --- a/extensions/data-workspace/src/common/constants.ts +++ b/extensions/data-workspace/src/common/constants.ts @@ -17,6 +17,13 @@ export const WorkspaceRequiredMessage = localize('dataworkspace.workspaceRequire export const OpenWorkspace = localize('dataworkspace.openWorkspace', "Open Workspace…"); export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A new workspace will be created and opened in order to open project. The Extension Host will restart and if there is a folder currently open, it will be closed."); export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspaceConfirmation', "To open this workspace, the Extension Host will restart and if there is a workspace or folder currently open, it will be closed."); +export const WorkspaceContainsNotAddedProjects = localize('dataworkspace.workspaceContainsNotAddedProjects', "The current workspace contains one or more projects that have not been added to the workspace. Use the 'Open existing' dialog to add projects to the projects pane."); +export const LaunchOpenExisitingDialog = localize('dataworkspace.launchOpenExistingDialog', "Launch Open existing dialog"); +export const DoNotShowAgain = localize('dataworkspace.doNotShowAgain', "Do not show again"); + +// config settings +export const projectsConfigurationKey = 'projects'; +export const showNotAddedProjectsMessageKey = 'showNotAddedProjectsInWorkspacePrompt'; // UI export const OkButtonText = localize('dataworkspace.ok', "OK"); diff --git a/extensions/data-workspace/src/main.ts b/extensions/data-workspace/src/main.ts index 86bb9ae5d3..7e87987f1f 100644 --- a/extensions/data-workspace/src/main.ts +++ b/extensions/data-workspace/src/main.ts @@ -16,6 +16,11 @@ import { IconPathHelper } from './common/iconHelper'; export function activate(context: vscode.ExtensionContext): Promise { const workspaceService = new WorkspaceService(context); workspaceService.loadTempProjects(); + workspaceService.checkForProjectsNotAddedToWorkspace(); + context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => { + workspaceService.checkForProjectsNotAddedToWorkspace(); + })); + const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService); const dataWorkspaceExtension = new DataWorkspaceExtension(workspaceService); context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider)); diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts index d129aab6d4..1d1b0146fb 100644 --- a/extensions/data-workspace/src/services/workspaceService.ts +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import * as dataworkspace from 'dataworkspace'; import * as path from 'path'; import * as constants from '../common/constants'; +import * as glob from 'fast-glob'; import { IWorkspaceService } from '../common/interfaces'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; import Logger from '../common/logger'; @@ -158,6 +159,65 @@ export class WorkspaceService implements IWorkspaceService { return vscode.workspace.workspaceFile ? this.getWorkspaceConfigurationValue(ProjectsConfigurationName).map(project => this.toUri(project)) : []; } + /** + * Check for projects that are in the workspace folders but have not been added to the workspace through the dialog or by editing the .code-workspace file + */ + async checkForProjectsNotAddedToWorkspace(): Promise { + const config = vscode.workspace.getConfiguration(constants.projectsConfigurationKey); + + // only check if the user hasn't selected not to show this prompt again + if (!config[constants.showNotAddedProjectsMessageKey]) { + return; + } + + // look for any projects that haven't been added to the workspace + const projectsInWorkspace = this.getProjectsInWorkspace(); + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders) { + return; + } + + for (const folder of workspaceFolders) { + const results = await this.getAllProjectsInWorkspaceFolder(folder); + + let containsNotAddedProject = false; + for (const projFile of results) { + // if any of the found projects aren't already in the workspace's projects, we can stop checking and show the info message + if (!projectsInWorkspace.find(p => p.fsPath === projFile)) { + containsNotAddedProject = true; + break; + } + } + + if (containsNotAddedProject) { + const result = await vscode.window.showInformationMessage(constants.WorkspaceContainsNotAddedProjects, constants.LaunchOpenExisitingDialog, constants.DoNotShowAgain); + if (result === constants.LaunchOpenExisitingDialog) { + // open settings + await vscode.commands.executeCommand('projects.openExisting'); + } else if (result === constants.DoNotShowAgain) { + await config.update(constants.showNotAddedProjectsMessageKey, false, true); + } + + return; + } + } + } + + async getAllProjectsInWorkspaceFolder(folder: vscode.WorkspaceFolder): Promise { + // get the unique supported project extensions + const supportedProjectExtensions = [...new Set((await this.getAllProjectTypes()).map(p => { return p.projectFileExtension; }))]; + + // path needs to use forward slashes for glob to work + const escapedPath = glob.escapePath(folder.uri.fsPath.replace(/\\/g, '/')); + + // can filter for multiple file extensions using folder/**/*.{sqlproj,csproj} format, but this notation doesn't work if there's only one extension + // so the filter needs to be in the format folder/**/*.sqlproj if there's only one supported projectextension + const projFilter = supportedProjectExtensions.length > 1 ? path.posix.join(escapedPath, '**', `*.{${supportedProjectExtensions.toString()}}`) : path.posix.join(escapedPath, '**', `*.${supportedProjectExtensions[0]}`); + + return await glob(projFilter); + } + async getProjectProvider(projectFile: vscode.Uri): Promise { const projectType = path.extname(projectFile.path).replace(/\./g, ''); let provider = ProjectProviderRegistry.getProviderByProjectExtension(projectType); diff --git a/extensions/data-workspace/src/test/workspaceService.test.ts b/extensions/data-workspace/src/test/workspaceService.test.ts index 2d4702b34a..12cfd1c1fe 100644 --- a/extensions/data-workspace/src/test/workspaceService.test.ts +++ b/extensions/data-workspace/src/test/workspaceService.test.ts @@ -10,10 +10,10 @@ import * as sinon from 'sinon'; import * as should from 'should'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; +import * as constants from '../common/constants'; import { WorkspaceService } from '../services/workspaceService'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; import { createProjectProvider } from './projectProviderRegistry.test'; -import { ProjectAlreadyOpened } from '../common/constants'; const DefaultWorkspaceFilePath = '/test/folder/ws.code-workspace'; @@ -223,7 +223,7 @@ suite('WorkspaceService Tests', function (): void { should.strictEqual(updateConfigurationStub.calledOnce, true, 'update configuration should have been called once'); should.strictEqual(updateWorkspaceFoldersStub.calledOnce, true, 'updateWorkspaceFolders should have been called once'); should.strictEqual(showInformationMessageStub.calledOnce, true, 'showInformationMessage should be called once'); - should(showInformationMessageStub.calledWith(ProjectAlreadyOpened(processPath('/test/folder/folder1/proj2.sqlproj')))).be.true(`showInformationMessage not called with expected message '${ProjectAlreadyOpened(processPath('/test/folder/folder1/proj2.sqlproj'))}' Actual '${showInformationMessageStub.getCall(0).args[0]}'`); + should(showInformationMessageStub.calledWith(constants.ProjectAlreadyOpened(processPath('/test/folder/folder1/proj2.sqlproj')))).be.true(`showInformationMessage not called with expected message '${constants.ProjectAlreadyOpened(processPath('/test/folder/folder1/proj2.sqlproj'))}' Actual '${showInformationMessageStub.getCall(0).args[0]}'`); should.strictEqual(updateConfigurationStub.calledWith('projects', sinon.match.array.deepEquals([ processPath('folder1/proj2.sqlproj'), processPath('proj1.sqlproj'), @@ -295,4 +295,30 @@ suite('WorkspaceService Tests', function (): void { onWorkspaceProjectsChangedDisposable.dispose(); }); + test('test checkForProjectsNotAddedToWorkspace', async () => { + const previousSetting = await vscode.workspace.getConfiguration(constants.projectsConfigurationKey)[constants.showNotAddedProjectsMessageKey]; + await vscode.workspace.getConfiguration(constants.projectsConfigurationKey).update(constants.showNotAddedProjectsMessageKey, true, true); + + sinon.stub(service, 'getProjectsInWorkspace').returns([vscode.Uri.file('abc.sqlproj'), vscode.Uri.file('folder1/abc1.sqlproj')]); + sinon.stub(vscode.workspace, 'workspaceFolders').value([{uri: vscode.Uri.file('.')}]); + sinon.stub(service, 'getAllProjectTypes').resolves([{ + projectFileExtension: 'sqlproj', + id: 'sql project', + displayName: 'sql project', + description: '', + icon: '' + }]); + const infoMessageStub = sinon.stub(vscode.window, 'showInformationMessage').resolves(constants.DoNotShowAgain); + const getProjectsInwWorkspaceFolderStub = sinon.stub(service, 'getAllProjectsInWorkspaceFolder').resolves([vscode.Uri.file('abc.sqlproj').fsPath, vscode.Uri.file('folder1/abc1.sqlproj').fsPath]); + + await service.checkForProjectsNotAddedToWorkspace(); + should(infoMessageStub.notCalled).be.true('Should not have found projects not added to workspace'); + + // add a project to the workspace folder not added to the workspace yet + getProjectsInwWorkspaceFolderStub.resolves([vscode.Uri.file('abc.sqlproj').fsPath, vscode.Uri.file('folder1/abc1.sqlproj').fsPath, vscode.Uri.file('folder2/abc2.sqlproj').fsPath]); + await service.checkForProjectsNotAddedToWorkspace(); + should(infoMessageStub.calledOnce).be.true('Should have found a project that was not added to the workspace'); + + await vscode.workspace.getConfiguration(constants.projectsConfigurationKey).update(constants.showNotAddedProjectsMessageKey, previousSetting, true); + }); }); diff --git a/extensions/data-workspace/yarn.lock b/extensions/data-workspace/yarn.lock index b8e748cecf..a70f4b6624 100644 --- a/extensions/data-workspace/yarn.lock +++ b/extensions/data-workspace/yarn.lock @@ -189,6 +189,27 @@ dependencies: vscode-extension-telemetry "^0.1.6" +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== + dependencies: + "@nodelib/fs.stat" "2.0.4" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + dependencies: + "@nodelib/fs.scandir" "2.1.4" + fastq "^1.6.0" + "@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2": version "1.8.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" @@ -299,6 +320,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -455,6 +483,32 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +fast-glob@^3.1.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fastq@^1.6.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb" + integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA== + dependencies: + reusify "^1.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -465,6 +519,13 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== +glob-parent@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -549,6 +610,23 @@ is-buffer@~1.1.1: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -668,6 +746,19 @@ md5@^2.1.0: crypt "~0.0.1" is-buffer "~1.1.1" +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -773,6 +864,11 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -790,6 +886,11 @@ resolve@^1.3.2: dependencies: path-parse "^1.0.6" +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -797,6 +898,11 @@ rimraf@^2.6.3: dependencies: glob "^7.1.3" +run-parallel@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" + integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== + safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -927,6 +1033,13 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"