Skip to content

Commit

Permalink
feat: move podman extension to packages/extension folder and add dev …
Browse files Browse the repository at this point in the history
…and runtime support for it (#8791)

Signed-off-by: Denis Golovin <[email protected]>
  • Loading branch information
dgolovin authored Sep 12, 2024
1 parent 9dd84a4 commit 017f68a
Show file tree
Hide file tree
Showing 64 changed files with 189 additions and 32 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ dist
/storybook/.storybook/themes.css
/storybook/storybook-static/
yarn.lock
extensions/podman/assets/
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const TYPESCRIPT_PROJECTS = [
'./website/tsconfig.json',
'./website-argos/tsconfig.json',
'./extensions/*/tsconfig.json',
'./extensions/*/packages/*/tsconfig.json',
'./tests/playwright/tsconfig.json',
'./tools/tsconfig.json',
'./storybook/tsconfig.json',
Expand All @@ -74,6 +75,8 @@ export default [
'scripts/**',
'extensions/*/builtin/**/*',
'extensions/*/scripts/**/*',
'extensions/*/packages/*/scripts/**/*',
'extensions/*/packages/*/builtin/**/*',
'website/storybook.ts',
'website/src/pages/storybook/sidebar.cjs',
],
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -17,19 +17,19 @@
***********************************************************************/

import path from 'node:path';
import { coverageConfig, testConfig } from '../../vitest-shared-extensions.config';
import { coverageConfig, testConfig } from '../../../../vitest-shared-extensions.config';

const PACKAGE_ROOT = __dirname;
const PACKAGE_NAME = 'extensions/podman';

const config = {
test: {
...testConfig(),
...coverageConfig(PACKAGE_ROOT, PACKAGE_NAME),
...testConfig(),
...coverageConfig(PACKAGE_ROOT, PACKAGE_NAME),
},
resolve: {
alias: {
'@podman-desktop/api': path.resolve(PACKAGE_ROOT, '../../', '__mocks__/@podman-desktop/api.js'),
'@podman-desktop/api': path.resolve(PACKAGE_ROOT, '../../../..', '__mocks__/@podman-desktop/api.js'),
},
},
};
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"build:extensions:kubecontext": "cd ./extensions/kube-context && npm run build",
"build:extensions:kind": "cd ./extensions/kind && npm run build",
"build:extensions:lima": "cd ./extensions/lima && npm run build",
"build:extensions:podman": "cd ./extensions/podman && npm run build",
"build:extensions:podman": "cd ./extensions/podman/packages/extension && npm run build",
"build:extensions:registries": "cd ./extensions/registries && npm run build",
"build:extensions:kubectl-cli": "cd ./extensions/kubectl-cli && npm run build",
"build:extension-api": "cd ./packages/extension-api && vite build",
Expand Down Expand Up @@ -57,7 +57,7 @@
"test:extensions:docker": "vitest run -r extensions/docker --passWithNoTests --coverage ",
"test:extensions:kube": "vitest run -r extensions/kube-context --passWithNoTests --coverage ",
"test:extensions:lima": "vitest run -r extensions/lima --passWithNoTests --coverage ",
"test:extensions:podman": "vitest run -r extensions/podman --passWithNoTests --coverage ",
"test:extensions:podman": "vitest run -r extensions/podman/packages/extension --passWithNoTests --coverage ",
"test:extensions:registries": "vitest run -r extensions/registries --passWithNoTests --coverage ",
"test:extensions:kubectl-cli": "vitest run -r extensions/kubectl-cli --passWithNoTests --coverage ",
"test:renderer": "npm run build:ui && vitest -c packages/renderer/vite.config.js run packages/renderer --passWithNoTests --coverage",
Expand Down Expand Up @@ -88,7 +88,7 @@
"typecheck:extensions:docker": "tsc --noEmit --project extensions/docker",
"typecheck:extensions:kube-context": "tsc --noEmit --project extensions/kube-context",
"typecheck:extensions:lima": "tsc --noEmit --project extensions/lima",
"typecheck:extensions:podman": "tsc --noEmit --project extensions/podman",
"typecheck:extensions:podman": "tsc --noEmit --project extensions/podman/packages/extension",
"typecheck:extensions:registries": "tsc --noEmit --project extensions/registries",
"typecheck:extensions:kubectl-cli": "tsc --noEmit --project extensions/kubectl-cli",
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:ui && npm run typecheck:renderer && npm run typecheck:preload-dd-extension && npm run typecheck:preload-webview && npm run typecheck:extensions && npm run typecheck:extension-api",
Expand Down
131 changes: 131 additions & 0 deletions packages/main/src/plugin/extension-loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2273,3 +2273,134 @@ test('when loading registry registerRegistry, do not push to disposables', async

expect(disposables.length).toBe(0);
});

describe('loading extension folders', () => {
const fileEntry = {
isDirectory: () => false,
} as fs.Dirent;
const nodeModulesEntry = {
isDirectory: () => true,
name: 'node_modules',
} as fs.Dirent;
const dirEntry = {
isDirectory: () => true,
name: 'extension1',
} as fs.Dirent;
const dirEntry2 = {
isDirectory: () => true,
name: 'extension2',
} as fs.Dirent;
const dirEntry3 = {
isDirectory: () => true,
name: 'extension3',
} as fs.Dirent;
const dirEntry4 = {
isDirectory: () => true,
name: 'extension4',
} as fs.Dirent;

describe('in dev mode', () => {
test('ignores files', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([fileEntry]);

const folders = await extensionLoader.readDevelopmentFolders('path');

expect(folders).length(0);
});
test('ignores node_modules folders', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([nodeModulesEntry]);

const folders = await extensionLoader.readDevelopmentFolders('path');

expect(folders).length(0);
});
test('ignores folders without package.json', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([dirEntry]);
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const folders = await extensionLoader.readDevelopmentFolders('path');

expect(folders).length(0);
});

test('recognizes a plain extension when only ext/package.json is present', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([dirEntry]);
vi.spyOn(fs, 'existsSync').mockReturnValueOnce(false).mockReturnValueOnce(true);
const folders = await extensionLoader.readDevelopmentFolders('path');

expect(folders).length(1);
expect(folders[0]).toBe(path.join('path', 'extension1'));
});

test('recognizes as an api extension when only ext/packages/extension/package.json is present', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([dirEntry]);
vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true).mockReturnValueOnce(true);
const folders = await extensionLoader.readDevelopmentFolders('path');

expect(folders).length(1);
expect(folders[0]).toBe(path.join('path', 'extension1', 'packages', 'extension'));
});

test('recognizes as an api extension when ext/package.json and ext/packages/extension/package.json are present', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([dirEntry]);
vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true).mockReturnValueOnce(false);
const folders = await extensionLoader.readDevelopmentFolders('path');

expect(folders).length(1);
expect(folders[0]).toBe(path.join('path', 'extension1', 'packages', 'extension'));
});

test('works correctly for multiple different extensions, files and empty folders', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([fileEntry, dirEntry, dirEntry2, dirEntry3, dirEntry4]);
vi.spyOn(fs, 'existsSync')
// an api extension
.mockReturnValueOnce(true)
// an plain extension
.mockReturnValueOnce(false) // plain extension
// priority to an api extension
.mockReturnValueOnce(true)
.mockReturnValueOnce(true) // priority to api extension
// ignore no package.json folders
.mockReturnValueOnce(false)
.mockReturnValueOnce(false);
const folders = await extensionLoader.readDevelopmentFolders('path');

expect(folders).length(3);
expect(folders[0]).toBe(path.join('path', 'extension1', 'packages', 'extension'));
expect(folders[1]).toBe(path.join('path', 'extension2'));
expect(folders[2]).toBe(path.join('path', 'extension3', 'packages', 'extension'));
});
});

describe('in prod mode', () => {
test('ignores files', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([fileEntry]);

const folders = await extensionLoader.readProductionFolders('path');

expect(folders).length(0);
});
test('ignores node_modules folders', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([nodeModulesEntry]);

const folders = await extensionLoader.readProductionFolders('path');

expect(folders).length(0);
});
test('recognizes a plain extension when only ext/package.json is present', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([dirEntry]);
vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true);
const folders = await extensionLoader.readProductionFolders('path');

expect(folders).length(1);
expect(folders[0]).toBe(path.join('path', 'extension1', 'builtin', 'extension1.cdix'));
});
test('recognizes an api extension when ext/package.json is not present', async () => {
vi.spyOn(fs.promises, 'readdir').mockResolvedValue([dirEntry]);
vi.spyOn(fs, 'existsSync').mockReturnValueOnce(false);
const folders = await extensionLoader.readProductionFolders('path');

expect(folders).length(1);
expect(folders[0]).toBe(path.join('path', 'extension1', 'packages', 'extension', 'builtin', `extension1.cdix`));
});
});
});
25 changes: 19 additions & 6 deletions packages/main/src/plugin/extension-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,17 @@ export class ExtensionLoader {
const entries = await fs.promises.readdir(folderPath, { withFileTypes: true });
// filter only directories ignoring node_modules directory
return entries
.filter(entry => entry.isDirectory())
.filter(directory => directory.name !== 'node_modules')
.map(directory => path.join(folderPath, directory.name));
.filter(entry => entry.isDirectory() && entry.name !== 'node_modules')
.reduce((directories: string[], directory) => {
const apiExtFolder = path.join(folderPath, directory.name, 'packages', 'extension');
const plainExtFolder = path.join(folderPath, directory.name);
if (fs.existsSync(path.join(apiExtFolder, 'package.json'))) {
directories.push(apiExtFolder);
} else if (fs.existsSync(path.join(plainExtFolder, 'package.json'))) {
directories.push(plainExtFolder);
}
return directories;
}, []);
}

async readExternalFolders(): Promise<string[]> {
Expand All @@ -602,9 +610,14 @@ export class ExtensionLoader {
async readProductionFolders(folderPath: string): Promise<string[]> {
const entries = await fs.promises.readdir(folderPath, { withFileTypes: true });
return entries
.filter(entry => entry.isDirectory())
.filter(directory => directory.name !== 'node_modules')
.map(directory => path.join(folderPath, directory.name, `/builtin/${directory.name}.cdix`));
.filter(entry => entry.isDirectory() && entry.name !== 'node_modules')
.map(directory => {
const rootExtPath = path.join(folderPath, directory.name);
const plainExtPath = path.join(rootExtPath, 'builtin', `${directory.name}.cdix`);
return fs.existsSync(plainExtPath)
? plainExtPath
: path.join(rootExtPath, 'packages', 'extension', 'builtin', `${directory.name}.cdix`);
});
}

/**
Expand Down
17 changes: 5 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
packages:
- 'packages/*'
- 'extensions/*'
- 'extensions/*/packages/*'
- 'storybook'
- 'website'
- 'website-argos'
Expand Down
23 changes: 19 additions & 4 deletions scripts/watch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,25 @@ const setupExtensionApiWatcher = name => {
// get extensions folder
const extensionsFolder = resolve(__dirname, '../extensions/');

// loop on all subfolders from the extensions folder
readdirSync(extensionsFolder, { withFileTypes: true })
.filter(dirent => dirent.isDirectory() && existsSync(join(extensionsFolder, dirent.name, 'package.json')))
.forEach(dirent => setupExtensionApiWatcher(join(extensionsFolder, dirent.name)));
// Loop on all subfolders from the extensions folder.
// If package.json is present it is an extension without API.
// If package.json is missing look into packages/extension folder
// and if package.json is present it is na extension with API.
readdirSync(extensionsFolder, {withFileTypes: true })
.filter(dirent =>
dirent.isDirectory() && (
existsSync(join(extensionsFolder, dirent.name, 'package.json'))
|| existsSync(extensionsFolder, dirent.name, 'packages', 'extension', 'package.json')
)
)
.forEach(dirent => {
const apiExtPath = join(extensionsFolder, dirent.name, 'packages', 'extension');
if(existsSync(join(apiExtPath, 'package.json'))) {
setupExtensionApiWatcher(apiExtPath);
} else if (existsSync(join(extensionsFolder, dirent.name, 'package.json'))) {
setupExtensionApiWatcher(join(extensionsFolder, dirent.name))
}
});

for (const extension of extensions) {
setupExtensionApiWatcher(extension);
Expand Down

0 comments on commit 017f68a

Please sign in to comment.