diff --git a/desktop/pkg/schemas/plugin-package-v2.json b/desktop/pkg/schemas/plugin-package-v2.json index b53d816fb..46bf0bc1e 100644 --- a/desktop/pkg/schemas/plugin-package-v2.json +++ b/desktop/pkg/schemas/plugin-package-v2.json @@ -8,7 +8,7 @@ "description": "The name of the package. Must start with \"flipper-plugin-\" prefix.", "type": "string", "maxLength": 214, - "pattern": "^flipper-plugin-[a-z0-9-._~]*$", + "pattern": "^(?:@[a-z0-9-*~][a-z0-9-*._~]*/)?flipper-plugin-[a-z0-9-._~]*$", "errorMessage": "should start with \"flipper-plugin-\", e.g. \"flipper-plugin-example\"" }, "id": { diff --git a/desktop/pkg/src/__tests__/runLint.node.ts b/desktop/pkg/src/__tests__/runLint.node.ts index e12d5715c..e5ad93d7d 100644 --- a/desktop/pkg/src/__tests__/runLint.node.ts +++ b/desktop/pkg/src/__tests__/runLint.node.ts @@ -48,6 +48,15 @@ test('valid package json', async () => { expect(result).toBe(null); }); +test('valid scoped package json', async () => { + const testPackageJson = Object.assign({}, validPackageJson); + testPackageJson.name = '@test/flipper-plugin-package'; + const json = JSON.stringify(testPackageJson); + fs.readFile = jest.fn().mockResolvedValue(new Buffer(json)); + const result = await runLint('dir'); + expect(result).toBe(null); +}); + test('$schema field is required', async () => { const testPackageJson = Object.assign({}, validPackageJson); delete testPackageJson.$schema; diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index 5a789f977..11d7bdb3e 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -45,11 +45,21 @@ function getPluginPendingInstallationDir( } function getPluginPendingInstallationsDir(name: string): string { - return path.join(pluginPendingInstallationDir, name); + return path.join( + pluginPendingInstallationDir, + replaceInvalidPathSegmentCharacters(name), + ); } function getPluginInstallationDir(name: string): string { - return path.join(pluginInstallationDir, name); + return path.join( + pluginInstallationDir, + replaceInvalidPathSegmentCharacters(name), + ); +} + +function replaceInvalidPathSegmentCharacters(name: string) { + return name.replace('/', '__'); } async function installPluginFromTempDir( @@ -145,7 +155,10 @@ export async function installPluginFromNpm(name: string) { const plugManNoDep = providePluginManagerNoDependencies(); plugManNoDep.options.pluginsPath = tmpDir; await plugManNoDep.install(name); - const pluginTempDir = path.join(tmpDir, name); + const pluginTempDir = path.join( + tmpDir, + replaceInvalidPathSegmentCharacters(name), + ); await installPluginFromTempDir(pluginTempDir); } finally { await fs.remove(tmpDir); @@ -178,14 +191,15 @@ export async function getInstalledPlugins(): Promise { const dirs = await fs.readdir(pluginInstallationDir); const plugins = await Promise.all<[string, PluginDetails]>( dirs.map( - (name) => + (dirName) => new Promise(async (resolve, reject) => { - const pluginDir = path.join(pluginInstallationDir, name); + const pluginDir = path.join(pluginInstallationDir, dirName); if (!(await fs.lstat(pluginDir)).isDirectory()) { return resolve(undefined); } try { - resolve([name, await getPluginDetails(pluginDir)]); + const details = await getPluginDetails(pluginDir); + resolve([details.name, details]); } catch (e) { reject(e); } @@ -203,24 +217,25 @@ export async function getPendingInstallationPlugins(): Promise { const dirs = await fs.readdir(pluginPendingInstallationDir); const plugins = await Promise.all<[string, PluginDetails]>( dirs.map( - (name) => + (dirName) => new Promise(async (resolve, reject) => { const versions = ( - await fs.readdir(path.join(pluginPendingInstallationDir, name)) + await fs.readdir(path.join(pluginPendingInstallationDir, dirName)) ).sort((v1, v2) => semver.compare(v2, v1, true)); if (versions.length === 0) { return resolve(undefined); } const pluginDir = path.join( pluginPendingInstallationDir, - name, + dirName, versions[0], ); if (!(await fs.lstat(pluginDir)).isDirectory()) { return resolve(undefined); } try { - resolve([name, await getPluginDetails(pluginDir)]); + const details = await getPluginDetails(pluginDir); + resolve([details.name, details]); } catch (e) { reject(e); } @@ -244,7 +259,7 @@ export async function getPendingAndInstalledPlugins(): Promise { } export async function removePlugin(name: string): Promise { - await fs.remove(path.join(pluginInstallationDir, name)); + await fs.remove(getPluginInstallationDir(name)); } export async function finishPendingPluginInstallations() {