init: sudah ganti logo, hilangin setting, dan investigational use dialog

This commit is contained in:
one
2025-03-06 11:32:45 +07:00
commit 8f31d4ed41
2857 changed files with 355646 additions and 0 deletions

3044
platform/cli/CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

44
platform/cli/package.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "@ohif/cli",
"version": "3.10.0-beta.111",
"description": "A CLI to bootstrap new OHIF extension or mode",
"type": "module",
"main": "src/index.js",
"private": true,
"bin": {
"ohif-cli": "src/index.js"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"cli",
"ohif"
],
"author": "OHIF Contributors",
"license": "MIT",
"dependencies": {
"@babel/core": "7.24.7",
"axios": "^0.28.0",
"chalk": "^5.0.0",
"execa": "^8.0.1",
"gitignore": "^0.7.0",
"inquirer": "^8.2.0",
"listr": "^0.14.3",
"mustache": "^4.2.0",
"ncp": "^2.0.0",
"node-fetch": "^3.1.1",
"pkg-install": "^1.0.0",
"registry-url": "^6.0.0",
"spdx-license-list": "^6.4.0",
"util": "^0.12.4",
"yarn-programmatic": "^0.1.2"
},
"files": [
"bin/",
"src/",
"templates/"
]
}

View File

@@ -0,0 +1,53 @@
import Listr from 'listr';
import chalk from 'chalk';
import {
installNPMPackage,
getYarnInfo,
validateExtension,
getVersionedPackageName,
addExtensionToConfig,
} from './utils/index.js';
export default async function addExtension(packageName, version) {
console.log(chalk.green.bold(`Adding ohif-extension ${packageName}...`));
const versionedPackageName = getVersionedPackageName(packageName, version);
const tasks = new Listr(
[
{
title: `Searching for extension: ${versionedPackageName}`,
task: async () => await validateExtension(packageName, version),
},
{
title: `Installing npm package: ${versionedPackageName}`,
task: async () => await installNPMPackage(packageName, version),
},
{
title: 'Adding ohif-extension to the configuration file',
task: async ctx => {
const yarnInfo = await getYarnInfo(packageName);
addExtensionToConfig(packageName, yarnInfo);
ctx.yarnInfo = yarnInfo;
},
},
],
{
exitOnError: true,
}
);
await tasks
.run()
.then(ctx => {
console.log(
`${chalk.green.bold(`Added ohif-extension ${packageName}@${ctx.yarnInfo.version}`)} `
);
})
.catch(error => {
console.log(error.message);
});
}

View File

@@ -0,0 +1,36 @@
import Listr from 'listr';
import chalk from 'chalk';
import addExtension from './addExtension.js';
export default async function addExtensions(ohifExtensions) {
// Auto generate Listr tasks...
const taskEntries = [];
ohifExtensions.forEach(({ packageName, version }) => {
const title = `Adding ohif-extension ${packageName}`;
taskEntries.push({
title,
task: async () => await addExtension(packageName, version),
});
});
const tasks = new Listr(taskEntries, {
exitOnError: true,
});
await tasks
.run()
.then(() => {
let extensonsString = '';
ohifExtensions.forEach(({ packageName, version }) => {
extensonsString += ` ${packageName}@${version}`;
});
console.log(`${chalk.green.bold(`Extensions added:${extensonsString}`)} `);
})
.catch(error => {
console.log(error.message);
});
}

View File

@@ -0,0 +1,66 @@
import Listr from 'listr';
import chalk from 'chalk';
import {
installNPMPackage,
getYarnInfo,
getVersionedPackageName,
validateMode,
addModeToConfig,
findRequiredOhifExtensionsForMode,
} from './utils/index.js';
import addExtensions from './addExtensions.js';
export default async function addMode(packageName, version) {
console.log(chalk.green.bold(`Adding ohif-mode ${packageName}...`));
const versionedPackageName = getVersionedPackageName(packageName, version);
const tasks = new Listr(
[
{
title: `Searching for mode: ${versionedPackageName}`,
task: async () => await validateMode(packageName, version),
},
{
title: `Installing npm package: ${versionedPackageName}`,
task: async () => await installNPMPackage(packageName, version),
},
{
title: 'Adding ohif-mode to the configuration file',
task: async ctx => {
const yarnInfo = await getYarnInfo(packageName);
addModeToConfig(packageName, yarnInfo);
ctx.yarnInfo = yarnInfo;
},
},
{
title: 'Detecting required ohif-extensions...',
task: async ctx => {
ctx.ohifExtensions = await findRequiredOhifExtensionsForMode(ctx.yarnInfo);
},
},
],
{
exitOnError: true,
}
);
await tasks
.run()
.then(async ctx => {
console.log(`${chalk.green.bold(`Added ohif-mode ${packageName}@${ctx.yarnInfo.version}`)} `);
const ohifExtensions = ctx.ohifExtensions;
if (ohifExtensions.length) {
console.log(`${chalk.green.bold(`Installing dependent extensions`)} `);
await addExtensions(ohifExtensions);
}
})
.catch(error => {
console.log(error.message);
});
}

View File

@@ -0,0 +1 @@
export default 'Not found';

View File

@@ -0,0 +1,77 @@
import Listr from 'listr';
import chalk from 'chalk';
import fs from 'fs';
import {
createDirectoryContents,
editPackageJson,
createLicense,
createReadme,
initGit,
} from './utils/index.js';
const createPackage = async options => {
const { packageType } = options; // extension or mode
if (fs.existsSync(options.targetDir)) {
console.error(
`%s ${packageType} with the same name already exists in this directory, either delete it or choose a different name`,
chalk.red.bold('ERROR')
);
process.exit(1);
}
fs.mkdirSync(options.targetDir);
const tasks = new Listr(
[
{
title: 'Copying template files',
task: () =>
createDirectoryContents(options.templateDir, options.targetDir, options.prettier),
},
{
title: 'Editing Package.json with provided information',
task: () => editPackageJson(options),
},
{
title: 'Creating a License file',
task: () => createLicense(options),
},
{
title: 'Creating a Readme file',
task: () => createReadme(options),
},
{
title: 'Initializing a Git Repository',
enabled: () => options.gitRepository,
task: () => initGit(options),
},
],
{
exitOnError: true,
}
);
await tasks.run();
console.log();
console.log(chalk.green(`Done: ${packageType} is ready at`, options.targetDir));
console.log();
console.log(chalk.green(`NOTE: In order to use this ${packageType} for development,`));
console.log(chalk.green(`run the following command inside the root of the OHIF monorepo`));
console.log();
console.log(chalk.green.bold(` yarn run cli link-${packageType} ${options.targetDir}`));
console.log();
console.log(
chalk.yellow("and when you don't need it anymore, run the following command to unlink it")
);
console.log();
console.log(chalk.yellow(` yarn run cli unlink-${packageType} ${options.name}`));
console.log();
return true;
};
export default createPackage;

View File

@@ -0,0 +1,8 @@
const colors = {
LIGHT: '#5acce6',
MAIN: '#0944b3',
DARK: '#090c29',
ACTIVE: '#348cfd',
};
export default colors;

View File

@@ -0,0 +1,5 @@
const endPoints = {
NPM_KEYWORD: 'https://registry.npmjs.com/-/v1/search?text=keywords:',
};
export default endPoints;

View File

@@ -0,0 +1,5 @@
import keywords from './keywords.js';
import colors from './colors.js';
import endPoints from './endPoints.js';
export { keywords, colors, endPoints };

View File

@@ -0,0 +1,6 @@
const keywords = {
MODE: 'ohif-mode',
EXTENSION: 'ohif-extension',
};
export default keywords;

View File

@@ -0,0 +1,23 @@
import createPackage from './createPackage.js';
import addExtension from './addExtension.js';
import removeExtension from './removeExtension.js';
import addMode from './addMode.js';
import removeMode from './removeMode.js';
import listPlugins from './listPlugins.js';
import searchPlugins from './searchPlugins.js';
import { linkExtension, linkMode } from './linkPackage.js';
import { unlinkExtension, unlinkMode } from './unlinkPackage.js';
export {
createPackage,
addExtension,
removeExtension,
addMode,
removeMode,
listPlugins,
searchPlugins,
linkExtension,
linkMode,
unlinkExtension,
unlinkMode,
};

View File

@@ -0,0 +1,79 @@
import fs from 'fs';
import path from 'path';
import { execa } from 'execa';
import { keywords } from './enums/index.js';
import { validateYarn, addExtensionToConfig, addModeToConfig } from './utils/index.js';
async function linkPackage(packageDir, options, addToConfig, keyword) {
const { viewerDirectory } = options;
// read package.json from packageDir
const file = fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8');
// name of the package
const packageJSON = JSON.parse(file);
const packageName = packageJSON.name;
const packageKeywords = packageJSON.keywords;
// check if package is an extension or a mode
if (!packageKeywords.includes(keyword)) {
throw new Error(`${packageName} is not ${keyword}`);
}
const version = packageJSON.version;
// make sure yarn is installed
await validateYarn();
// change directory to packageDir and execute yarn link
process.chdir(packageDir);
let results;
results = await execa(`yarn`, ['link']);
// change directory to OHIF Platform root and execute yarn link
process.chdir(`${viewerDirectory}/../..`);
results = await execa(`yarn`, ['link', packageName]);
console.log(results.stdout);
// Add the node_modules of the linked package so that webpack
// can find the linked package externals if there are
const webpackPwaPath = path.join(viewerDirectory, '.webpack', 'webpack.pwa.js');
async function updateWebpackConfig(webpackConfigPath, packageDir) {
const packageNodeModules = path.join(packageDir, 'node_modules');
const fileContent = await fs.promises.readFile(webpackConfigPath, 'utf8');
const newLine = `path.resolve(__dirname, '${packageNodeModules}'),`;
const modifiedFileContent = fileContent.replace(
/(modules:\s*\[)([\s\S]*?)(\])/,
`$1$2 ${newLine.replace(/\\/g, '/')}$3`
);
await fs.promises.writeFile(webpackConfigPath, modifiedFileContent);
}
await updateWebpackConfig(webpackPwaPath, packageDir);
// change directory to viewer packages and add the config item
process.chdir(viewerDirectory);
addToConfig(packageName, {
version,
});
// run prettier on the webpack config
results = await execa(`yarn`, ['prettier', '--write', webpackPwaPath]);
}
function linkExtension(packageDir, options) {
const keyword = keywords.EXTENSION;
linkPackage(packageDir, options, addExtensionToConfig, keyword);
}
function linkMode(packageDir, options) {
const keyword = keywords.MODE;
linkPackage(packageDir, options, addModeToConfig, keyword);
}
export { linkExtension, linkMode };

View File

@@ -0,0 +1,23 @@
import fs from 'fs';
import { prettyPrint } from './utils/index.js';
import { colors } from './enums/index.js';
const listPlugins = async configPath => {
const pluginConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const { extensions, modes } = pluginConfig;
const titleOptions = { color: colors.LIGHT, bold: true };
const itemsOptions = { color: colors.ACTIVE, bold: true };
const extensionsItems = extensions.map(
extension => `${extension.packageName} @ ${extension.version}`
);
const modesItems = modes.map(mode => `${mode.packageName} @ ${mode.version}`);
prettyPrint('Extensions', titleOptions, extensionsItems, itemsOptions);
prettyPrint('Modes', titleOptions, modesItems, itemsOptions);
};
export default listPlugins;

View File

@@ -0,0 +1,46 @@
import chalk from 'chalk';
import Listr from 'listr';
import {
uninstallNPMPackage,
throwIfExtensionUsedByInstalledMode,
removeExtensionFromConfig,
validateExtensionYarnInfo,
} from './utils/index.js';
export default async function removeExtension(packageName) {
console.log(chalk.green.bold(`Removing ohif-extension ${packageName}...`));
const tasks = new Listr(
[
{
title: `Searching for installed extension: ${packageName}`,
task: async () => await validateExtensionYarnInfo(packageName),
},
{
title: `Checking if ${packageName} is in use by an installed mode`,
task: async () => await throwIfExtensionUsedByInstalledMode(packageName),
},
{
title: `Uninstalling npm package: ${packageName}`,
task: async () => await uninstallNPMPackage(packageName),
},
{
title: 'Removing ohif-extension from the configuration file',
task: async () => removeExtensionFromConfig(packageName),
},
],
{
exitOnError: true,
}
);
await tasks
.run()
.then(() => {
console.log(`${chalk.green.bold(`Removed ohif-extension ${packageName}`)} `);
})
.catch(error => {
console.log(error.message);
});
}

View File

@@ -0,0 +1,36 @@
import Listr from 'listr';
import chalk from 'chalk';
import removeExtension from './removeExtension.js';
export default async function removeExtensions(ohifExtensionsToRemove) {
// Auto generate Listr tasks...
const taskEntries = [];
ohifExtensionsToRemove.forEach(packageName => {
const title = `Removing ohif-extension ${packageName}`;
taskEntries.push({
title,
task: async () => await removeExtension(packageName),
});
});
const tasks = new Listr(taskEntries, {
exitOnError: true,
});
await tasks
.run()
.then(() => {
let extensonsString = '';
ohifExtensionsToRemove.forEach(packageName => {
extensonsString += ` ${packageName}`;
});
console.log(`${chalk.green.bold(`Extensions removed:${extensonsString}`)} `);
})
.catch(error => {
console.log(error.message);
});
}

View File

@@ -0,0 +1,68 @@
import Listr from 'listr';
import chalk from 'chalk';
import {
uninstallNPMPackage,
findOhifExtensionsToRemoveAfterRemovingMode,
removeModeFromConfig,
validateModeYarnInfo,
getYarnInfo,
} from './utils/index.js';
import removeExtensions from './removeExtensions.js';
export default async function removeMode(packageName) {
console.log(chalk.green.bold(`Removing ohif-mode ${packageName}...`));
const tasks = new Listr(
[
{
title: `Searching for installed mode: ${packageName}`,
task: async ctx => {
ctx.yarnInfo = await getYarnInfo(packageName);
await validateModeYarnInfo(packageName);
},
},
{
title: `Uninstalling npm package: ${packageName}`,
task: async () => await uninstallNPMPackage(packageName),
},
{
title: 'Removing ohif-mode from the configuration file',
task: async () => await removeModeFromConfig(packageName),
},
{
title: 'Detecting extensions that can be removed...',
task: async ctx => {
ctx.ohifExtensionsToRemove = await findOhifExtensionsToRemoveAfterRemovingMode(
ctx.yarnInfo
);
},
},
],
{
exitOnError: true,
}
);
await tasks
.run()
.then(async ctx => {
// Remove extensions if they aren't used by any other mode.
console.log(`${chalk.green.bold(`Removed ohif-mode ${packageName}`)} `);
const ohifExtensionsToRemove = ctx.ohifExtensionsToRemove;
if (ohifExtensionsToRemove.length) {
console.log(
`${chalk.green.bold(
`Removing ${ohifExtensionsToRemove.length} extensions no longer used by any installed mode`
)}`
);
await removeExtensions(ohifExtensionsToRemove);
}
})
.catch(error => {
console.log(error.message);
});
}

View File

@@ -0,0 +1,57 @@
import axios from 'axios';
import { prettyPrint } from './utils/index.js';
import { keywords, colors, endPoints } from './enums/index.js';
async function searchRegistry(keyword) {
const url = `${endPoints.NPM_KEYWORD}${keyword}`;
try {
const response = await axios.get(url);
const { objects } = response.data;
return objects;
} catch (error) {
console.log(error);
}
}
async function searchPlugins(options) {
const { verbose } = options;
const extensions = await searchRegistry(keywords.EXTENSION);
const modes = await searchRegistry(keywords.MODE);
const titleOptions = { color: colors.LIGHT, bold: true };
const itemsOptions = {};
const extensionsItems = extensions.map(extension => {
const item = [
`${extension.package.name} @ ${extension.package.version}`,
[`Description: ${extension.package.description}`],
];
if (verbose) {
item[1].push(`Repository: ${extension.package.links.repository}`);
}
return item;
});
const modesItems = modes.map(mode => {
const item = [
`${mode.package.name} @ ${mode.package.version}`,
[`Description: ${mode.package.description}`],
];
if (verbose) {
item[1].push(`Repository: ${mode.package.links.repository}`);
}
return item;
});
prettyPrint('Extensions', titleOptions, extensionsItems, itemsOptions);
prettyPrint('Modes', titleOptions, modesItems, itemsOptions);
}
export default searchPlugins;

View File

@@ -0,0 +1,66 @@
import { execa } from 'execa';
import fs from 'fs';
import path from 'path';
import { validateYarn, removeExtensionFromConfig, removeModeFromConfig } from './utils/index.js';
const linkPackage = async (packageName, options, removeFromConfig) => {
const { viewerDirectory } = options;
// make sure yarn is installed
await validateYarn();
// change directory to OHIF Platform root and execute yarn link
process.chdir(viewerDirectory);
const results = await execa(`yarn`, ['unlink', packageName]);
console.log(results.stdout);
const webpackPwaPath = path.join(viewerDirectory, '.webpack', 'webpack.pwa.js');
await removePathFromWebpackConfig(webpackPwaPath, packageName);
//update the plugin.json file
removeFromConfig(packageName);
// run prettier on the webpack config
await execa(`yarn`, ['prettier', '--write', webpackPwaPath]);
};
async function removePathFromWebpackConfig(webpackConfigPath, packageName) {
const fileContent = await fs.promises.readFile(webpackConfigPath, 'utf8');
const packageNameSubstring = `${packageName}/node_modules`;
const pathResolveStart = 'path.resolve(';
const closingParenthesis = ')';
let startIndex = fileContent.indexOf(packageNameSubstring);
if (startIndex === -1) {
return;
}
// Find the start of the "path.resolve" line.
startIndex = fileContent.lastIndexOf(pathResolveStart, startIndex);
// Find the end of the line with the closing parenthesis.
let endIndex = fileContent.indexOf(closingParenthesis, startIndex) + 1;
// Check if there's a comma after the closing parenthesis and remove it as well.
if (fileContent[endIndex] === ',') {
endIndex++;
}
const modifiedFileContent = fileContent.slice(0, startIndex) + fileContent.slice(endIndex);
await fs.promises.writeFile(webpackConfigPath, modifiedFileContent);
}
function unlinkExtension(extensionName, options) {
linkPackage(extensionName, options, removeExtensionFromConfig);
}
function unlinkMode(modeName, options) {
linkPackage(modeName, options, removeModeFromConfig);
}
export { unlinkExtension, unlinkMode };

View File

@@ -0,0 +1,34 @@
import {
addExtensionToConfigJson,
addModeToConfigJson,
readPluginConfigFile,
writePluginConfigFile,
} from './private/index.js';
function addToAndOverwriteConfig(packageName, options, augmentConfigFunction) {
const installedVersion = options.version;
let pluginConfig = readPluginConfigFile();
if (!pluginConfig) {
pluginConfig = {
extensions: [],
modes: [],
};
}
augmentConfigFunction(pluginConfig, {
packageName,
version: installedVersion,
});
writePluginConfigFile(pluginConfig);
}
function addExtensionToConfig(packageName, options) {
addToAndOverwriteConfig(packageName, options, addExtensionToConfigJson);
}
function addModeToConfig(packageName, options) {
addToAndOverwriteConfig(packageName, options, addModeToConfigJson);
}
export { addExtensionToConfig, addModeToConfig };

View File

@@ -0,0 +1,40 @@
import fs from 'fs';
// https://github.dev/leoroese/template-cli/blob/628dd24db7df399ebb520edd0bc301bc7b5e8b66/index.js#L19
const createDirectoryContents = (templatePath, targetDirPath, copyPrettierRules) => {
const filesToCreate = fs.readdirSync(templatePath);
filesToCreate.forEach(file => {
if (!copyPrettierRules && file === '.prettierrc') {
return;
}
const origFilePath = `${templatePath}/${file}`;
// get stats about the current file
const stats = fs.statSync(origFilePath);
if (stats.isFile()) {
const contents = fs.readFileSync(origFilePath, 'utf8');
// Rename
if (file === '.npmignore') {
file = '.gitignore';
}
const writePath = `${targetDirPath}/${file}`;
fs.writeFileSync(writePath, contents, 'utf8');
} else if (stats.isDirectory()) {
fs.mkdirSync(`${targetDirPath}/${file}`);
// recursive call
createDirectoryContents(
`${templatePath}/${file}`,
`${targetDirPath}/${file}`,
copyPrettierRules
);
}
});
};
export default createDirectoryContents;

View File

@@ -0,0 +1,31 @@
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import spdxLicenseList from 'spdx-license-list/full.js';
const writeFile = promisify(fs.writeFile);
async function createLicense(options) {
const { targetDir, name, email } = options;
const targetPath = path.join(targetDir, 'LICENSE');
let license;
try {
license = spdxLicenseList[options.license];
} catch (err) {
console.error(
'%s License %s not found in the list of licenses',
chalk.red.bold('ERROR'),
options.license
);
process.exit(1);
}
const licenseContent = license.licenseText
.replace('<year>', new Date().getFullYear())
.replace('<copyright holders>', `${name} (${email})`);
return writeFile(targetPath, licenseContent, 'utf8');
}
export default createLicense;

View File

@@ -0,0 +1,23 @@
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import mustache from 'mustache';
const writeFile = promisify(fs.writeFile);
async function createReadme(options) {
let template = `# {{name}} \n## Description \n{{description}} \n## Author \n{{author}} \n## License \n{{license}}`;
const { name, description, author, license, targetDir } = options;
const targetPath = path.join(targetDir, 'README.md');
const readmeContent = mustache.render(template, {
name,
description,
author,
license,
});
return writeFile(targetPath, readmeContent, 'utf8');
}
export default createReadme;

View File

@@ -0,0 +1,38 @@
import fs from 'fs';
import path from 'path';
async function editPackageJson(options) {
const { name, version, description, author, license, targetDir } = options;
const ohifVersion = fs.readFileSync('./version.txt', 'utf8').trim();
// read package.json from targetDir
const dependenciesPath = path.join(targetDir, 'dependencies.json');
const rawData = fs.readFileSync(dependenciesPath, 'utf8');
const dataWithOHIFVersion = rawData.replace(/\{LATEST_OHIF_VERSION\}/g, ohifVersion);
const packageJson = JSON.parse(dataWithOHIFVersion);
// edit package.json
const mergedObj = Object.assign(
{
name,
version,
description,
author,
license,
main: `dist/umd/${name}/index.umd.js`,
files: ['dist/**', 'public/**', 'README.md'],
},
packageJson
);
// write package.json back to targetDir
const writePath = path.join(targetDir, 'package.json');
fs.writeFileSync(writePath, JSON.stringify(mergedObj, null, 2));
// remove the dependencies.json file
fs.unlinkSync(dependenciesPath);
}
export default editPackageJson;

View File

@@ -0,0 +1,59 @@
import { readPluginConfigFile } from './private/index.js';
import getYarnInfo from './getYarnInfo.js';
export default async function findOhifExtensionsToRemoveAfterRemovingMode(removedModeYarnInfo) {
const pluginConfig = readPluginConfigFile();
if (!pluginConfig) {
// No other modes or extensions, no action item.
return [];
}
const { modes, extensions } = pluginConfig;
const registeredExtensions = extensions.map(extension => extension.packageName);
// TODO this is not a function
const ohifExtensionsOfMode = Object.keys(removedModeYarnInfo.peerDependencies).filter(
peerDependency => registeredExtensions.includes(peerDependency)
);
const ohifExtensionsUsedInOtherModes = ohifExtensionsOfMode.map(packageName => {
return {
packageName,
used: false,
};
});
// Check if other modes use each extension used by this mode
const otherModes = modes.filter(mode => mode.packageName !== removedModeYarnInfo.name);
for (let i = 0; i < otherModes.length; i++) {
const mode = otherModes[i];
const yarnInfo = await getYarnInfo(mode.packageName);
const peerDependencies = yarnInfo.peerDependencies;
if (!peerDependencies) {
continue;
}
for (let j = 0; j < ohifExtensionsUsedInOtherModes.length; j++) {
const ohifExtension = ohifExtensionsUsedInOtherModes[j];
if (ohifExtension.used) {
// Already accounted that we can't delete this, so don't waste effort
return;
}
if (Object.keys(peerDependencies).includes(ohifExtension.packageName)) {
ohifExtension.used = true;
}
}
}
// Return list of now unused extensions
const ohifExtensionsToRemove = ohifExtensionsUsedInOtherModes
.filter(ohifExtension => !ohifExtension.used)
.map(ohifExtension => ohifExtension.packageName);
return ohifExtensionsToRemove;
}

View File

@@ -0,0 +1,41 @@
import { validateExtension } from './validate.js';
export default async function findRequiredOhifExtensionsForMode(yarnInfo) {
// Get yarn info file and get peer dependencies
if (!yarnInfo.peerDependencies) {
// No ohif-extension dependencies
return;
}
const peerDependencies = yarnInfo.peerDependencies;
const dependencies = [];
const ohifExtensions = [];
Object.keys(peerDependencies).forEach(packageName => {
dependencies.push({
packageName,
version: peerDependencies[packageName],
});
});
const promises = [];
// Fetch each npm json and check which are ohif extensions
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
const { packageName, version } = dependency;
const promise = validateExtension(packageName, version)
.then(() => {
ohifExtensions.push({ packageName, version });
})
.catch(() => {});
promises.push(promise);
}
// Await all the extensions // TODO -> Improve so we async install each
// extension and await all of those promises instead.
await Promise.all(promises);
return ohifExtensions;
}

View File

@@ -0,0 +1,3 @@
export default function getVersionedPackageName(packageName, version) {
return version === undefined ? packageName : `${packageName}@${version}`;
}

View File

@@ -0,0 +1,5 @@
import { info } from 'yarn-programmatic';
export default async function getYarnInfo(packageName) {
return await info(packageName);
}

View File

@@ -0,0 +1,47 @@
import getVersionedPackageName from './getVersionedPackageName.js';
import installNPMPackage from './installNPMPackage.js';
import uninstallNPMPackage from './uninstallNPMPackage.js';
import {
validateMode,
validateExtension,
validateModeYarnInfo,
validateExtensionYarnInfo,
} from './validate.js';
import getYarnInfo from './getYarnInfo.js';
import { addExtensionToConfig, addModeToConfig } from './addToConfig.js';
import findRequiredOhifExtensionsForMode from './findRequiredOhifExtensionsForMode.js';
import { removeExtensionFromConfig, removeModeFromConfig } from './removeFromConfig.js';
import throwIfExtensionUsedByInstalledMode from './throwIfExtensionUsedByInstalledMode.js';
import findOhifExtensionsToRemoveAfterRemovingMode from './findOhifExtensionsToRemoveAfterRemovingMode.js';
import initGit from './initGit.js';
import createDirectoryContents from './createDirectoryContents.js';
import editPackageJson from './editPackageJson.js';
import createLicense from './createLicense.js';
import createReadme from './createReadme.js';
import prettyPrint from './prettyPrint.js';
import validateYarn from './validateYarn.js';
export {
getYarnInfo,
getVersionedPackageName,
installNPMPackage,
uninstallNPMPackage,
validateMode,
validateExtension,
validateModeYarnInfo,
validateExtensionYarnInfo,
addExtensionToConfig,
addModeToConfig,
findRequiredOhifExtensionsForMode,
removeExtensionFromConfig,
throwIfExtensionUsedByInstalledMode,
removeModeFromConfig,
findOhifExtensionsToRemoveAfterRemovingMode,
initGit,
createDirectoryContents,
editPackageJson,
createLicense,
createReadme,
prettyPrint,
validateYarn,
};

View File

@@ -0,0 +1,35 @@
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import { execa } from 'execa';
const exists = promisify(fs.exists);
async function initGit(options) {
const { targetDir } = options;
const targetPath = path.join(targetDir, '.git');
// Check if git is installed
try {
await execa('git', ['--version']);
} catch (err) {
console.error(
'%s Git is not installed. Please install git and try again.',
chalk.red.bold('ERROR')
);
process.exit(1);
}
if (!(await exists(targetPath))) {
try {
await execa('git', ['init'], { cwd: targetDir });
} catch (err) {
console.error('%s Failed to initialize git', chalk.red.bold('ERROR'));
console.error(err);
process.exit(1);
}
}
}
export default initGit;

View File

@@ -0,0 +1,14 @@
import { install } from 'pkg-install';
const installNPMPackage = async (packageName, version) => {
let installObject = {};
installObject[packageName] = version;
await install(installObject, {
prefer: 'yarn',
cwd: process.cwd(),
});
};
export default installNPMPackage;

View File

@@ -0,0 +1,79 @@
import chalk from 'chalk';
import { colors } from '../enums/index.js';
function getStyle({ color, bold }) {
return bold ? chalk.hex(color).bold : chalk.hex(color);
}
function levelOnePrint(items) {
let output = '';
if (Array.isArray(items)) {
items.forEach(item => {
output += ` |- ${item}\n`;
});
return output;
}
return ` |- ${items}\n`;
}
function levelTwoPrint(items) {
let output = '';
items.forEach(item => {
output += ` | |- ${item}\n`;
});
return output;
}
/**
*
* @param {string} title Title of the section
* @param {object} titleOptions Options for the title includes color and bold
* @param { [] | [][] } items Array of items to display, OR a list of lists
* @param {object} itemOptions Options for the items includes color and bold
*
*
* items= ['Mode-A', 'Mode-B', 'Mode-C']
*
* |- Mode-A
* |- Mode-B
* |- Mode-C
*
* items = [['Mode-A', ['Description-A', 'Authors-A', 'Repository-A]], ['Mode-B', ['Description-B', 'Authors-B', 'Repository-B]], ['Mode-C', ['Description-C', 'Authors-C', 'Repository-C]]]
*
* |- Mode-A
* | |- Description-A
* | |- Authors-A
* | |- Repository-A
* |
* |- Mode-B
* | |- Description-B
* | |- Authors-B
* | |- Repository-B
*
*
*/
function prettyPrint(
title,
titleOptions = { color: colors.MAIN, bold: true },
itemsArray = [[]],
itemOptions = {}
) {
console.log('');
console.log(getStyle(titleOptions)(title));
let output = '';
itemsArray.forEach(items => {
if (!Array.isArray(items)) {
output += levelOnePrint(items);
} else {
output += levelOnePrint(items[0]);
output += levelTwoPrint(items[1]);
}
});
const itmeStyle = itemOptions.color ? getStyle(itemOptions)(output) : output;
console.log(itmeStyle);
}
export default prettyPrint;

View File

@@ -0,0 +1,15 @@
export default function getPackageNameAndScope(packageName) {
let scope;
let packageNameLessScope;
if (packageName.includes('@')) {
[scope, packageNameLessScope] = packageName.split('/');
} else {
packageNameLessScope = packageName;
}
return {
scope,
packageNameLessScope,
};
}

View File

@@ -0,0 +1,19 @@
import getPackageNameAndScope from './getPackageNameAndScope.js';
import {
addExtensionToConfigJson,
removeExtensionFromConfigJson,
addModeToConfigJson,
removeModeFromConfigJson,
} from './manipulatePluginConfigFile.js';
import writePluginConfigFile from './writePluginConfigFile.js';
import readPluginConfigFile from './readPluginConfigFile.js';
export {
getPackageNameAndScope,
addExtensionToConfigJson,
removeExtensionFromConfigJson,
addModeToConfigJson,
removeModeFromConfigJson,
readPluginConfigFile,
writePluginConfigFile,
};

View File

@@ -0,0 +1,38 @@
function addExtensionToConfigJson(pluginConfig, { packageName, version }) {
addToList('extensions', pluginConfig, { packageName, version });
}
function addModeToConfigJson(pluginConfig, { packageName, version }) {
addToList('modes', pluginConfig, { packageName, version });
}
function removeExtensionFromConfigJson(pluginConfig, { packageName }) {
removeFromList('extensions', pluginConfig, { packageName });
}
function removeModeFromConfigJson(pluginConfig, { packageName }) {
removeFromList('modes', pluginConfig, { packageName });
}
function removeFromList(listName, pluginConfig, { packageName }) {
const list = pluginConfig[listName];
const indexOfExistingEntry = list.findIndex(entry => entry.packageName === packageName);
if (indexOfExistingEntry !== -1) {
pluginConfig[listName].splice(indexOfExistingEntry, 1);
}
}
function addToList(listName, pluginConfig, { packageName, version }) {
removeFromList(listName, pluginConfig, { packageName });
pluginConfig[listName].push({ packageName, version });
}
export {
addExtensionToConfigJson,
addModeToConfigJson,
removeExtensionFromConfigJson,
removeModeFromConfigJson,
};

View File

@@ -0,0 +1,17 @@
import fs from 'fs';
export default function readPluginConfigFile() {
let fileContents;
try {
fileContents = fs.readFileSync('./pluginConfig.json', { flag: 'r' });
} catch (err) {
return; // File doesn't exist yet.
}
if (fileContents) {
fileContents = JSON.parse(fileContents);
}
return fileContents;
}

View File

@@ -0,0 +1,18 @@
import fs from 'fs';
export default function writePluginConfigFile(pluginConfig) {
// Note: Second 2 arguments are to pretty print the JSON so its human readable.
const jsonStringOfFileContents = JSON.stringify(pluginConfig, null, 2);
fs.writeFileSync(
`./pluginConfig.json`,
jsonStringOfFileContents + '\n', // Add a newline character at the end
{ flag: 'w+' },
err => {
if (err) {
console.error(err);
return;
}
}
);
}

View File

@@ -0,0 +1,26 @@
import {
removeExtensionFromConfigJson,
removeModeFromConfigJson,
writePluginConfigFile,
readPluginConfigFile,
} from './private/index.js';
function removeFromAndOverwriteConfig(packageName, augmentConfigFunction) {
const pluginConfig = readPluginConfigFile();
// Note: if file is not found, nothing to remove.
if (pluginConfig) {
augmentConfigFunction(pluginConfig, { packageName });
writePluginConfigFile(pluginConfig);
}
}
function removeExtensionFromConfig(packageName) {
removeFromAndOverwriteConfig(packageName, removeExtensionFromConfigJson);
}
function removeModeFromConfig(packageName) {
removeFromAndOverwriteConfig(packageName, removeModeFromConfigJson);
}
export { removeExtensionFromConfig, removeModeFromConfig };

View File

@@ -0,0 +1,48 @@
import { readPluginConfigFile } from './private/index.js';
import getYarnInfo from './getYarnInfo.js';
import chalk from 'chalk';
export default async function throwIfExtensionUsedByInstalledMode(packageName) {
const pluginConfig = readPluginConfigFile();
if (!pluginConfig) {
// No other modes, not in use
return false;
}
const { modes } = pluginConfig;
const modesUsingExtension = [];
for (let i = 0; i < modes.length; i++) {
const mode = modes[i];
const modePackageName = mode.packageName;
const yarnInfo = await getYarnInfo(modePackageName);
const peerDependencies = yarnInfo.peerDependencies;
if (!peerDependencies) {
continue;
}
if (Object.keys(peerDependencies).includes(packageName)) {
modesUsingExtension.push(modePackageName);
}
}
if (modesUsingExtension.length > 0) {
let modesString = '';
modesUsingExtension.forEach(packageName => {
modesString += ` ${packageName}`;
});
const error = new Error(
`${chalk.yellow.red(
'Error'
)} ohif-extension ${packageName} used by installed modes:${modesString}`
);
throw error;
}
}

View File

@@ -0,0 +1,12 @@
import { remove } from 'yarn-programmatic';
const uninstallNPMPackage = async packageName => {
// TODO - Anoyingly pkg-install doesn't seem to have uninstall.
// So since we are using yarn we will just use yarn here, but the tool
// is certainly less generic. But its a super minor issue.
await remove(packageName).catch(err => {
console.log(err);
});
};
export default uninstallNPMPackage;

View File

@@ -0,0 +1,152 @@
import registryUrl from 'registry-url';
import keywords from '../enums/keywords.js';
import { getPackageNameAndScope } from './private/index.js';
import chalk from 'chalk';
import fetch from 'node-fetch';
import getYarnInfo from './getYarnInfo.js';
import NOT_FOUND from '../constants/notFound.js';
async function validateMode(packageName, version) {
return validate(packageName, version, keywords.MODE);
}
async function validateExtension(packageName, version) {
return validate(packageName, version, keywords.EXTENSION);
}
async function validateModeYarnInfo(packageName) {
return validateYarnInfo(packageName, keywords.MODE);
}
async function validateExtensionYarnInfo(packageName) {
return validateYarnInfo(packageName, keywords.EXTENSION);
}
function validateYarnInfo(packageName, keyword) {
return new Promise(async (resolve, reject) => {
function rejectIfNotFound() {
const error = new Error(`${chalk.red.bold('Error')} extension ${packageName} not installed`);
reject(error);
}
const packageInfo = await getYarnInfo(packageName).catch(() => {
rejectIfNotFound();
});
if (!packageInfo) {
rejectIfNotFound();
return;
}
const { keywords } = packageInfo;
const isValid = keywords && keywords.includes(keyword);
if (isValid) {
resolve(true);
} else {
const error = new Error(
`${chalk.red.bold('Error')} package ${packageName} is not an ${keyword}`
);
reject(error);
}
});
}
function getVersion(json, version) {
const versions = Object.keys(json.versions);
// if no version is defined get the latest
if (version === undefined) {
return json['dist-tags'].latest;
}
// Get and validate version if it is explicitly defined
const allowMinorVersionUpgrade = version.startsWith('^');
if (!allowMinorVersionUpgrade) {
const isValidVersion = versions.includes(version);
if (!isValidVersion) {
return;
}
return version;
}
// Choose version based on the newer minor/patch versions
const [majorVersion] = version
.split('^')[1]
.split('.')
.map(v => parseInt(v));
// Find the version that matches the major version, but is the latest minor version
versions
.filter(version => parseInt(version.split('.')[0]) === majorVersion)
.sort((a, b) => {
const [majorA, minorA, patchA] = a.split('.').map(v => parseInt(v));
const [majorB, minorB, patchB] = b.split('.').map(v => parseInt(v));
if (majorA === majorB) {
if (minorA === minorB) {
return patchB - patchA;
}
return minorB - minorA;
}
return majorB - majorA;
});
if (versions.length === 0) {
return;
}
return versions[0];
}
function validate(packageName, version, keyword) {
return new Promise(async (resolve, reject) => {
const { scope } = getPackageNameAndScope(packageName);
// Gets the registry of the package. Scoped packages may not be using the global default.
const registryUrlOfPackage = registryUrl(scope);
let options = {};
if (process.env.NPM_TOKEN) {
options['headers'] = {
Authorization: `Bearer ${process.env.NPM_TOKEN}`,
};
}
const response = await fetch(`${registryUrlOfPackage}${packageName}`, options);
const json = await response.json();
if (json.error && json.error === NOT_FOUND) {
const error = new Error(`${chalk.red.bold('Error')} package ${packageName} not found`);
reject(error);
return;
}
const packageVersion = getVersion(json, version);
if (packageVersion) {
const versionedJson = json.versions[packageVersion];
const keywords = versionedJson.keywords;
const isValid = keywords && keywords.includes(keyword);
if (isValid) {
resolve(true);
} else {
const error = new Error(
`${chalk.red.bold('Error')} package ${packageName} is not an ${keyword}`
);
reject(error);
}
} else {
// Particular version undefined
const error = new Error(
`${chalk.red.bold('Error')} version ${packageVersion} of package ${packageName} not found`
);
reject(error);
}
});
}
export { validateMode, validateExtension, validateModeYarnInfo, validateExtensionYarnInfo };

View File

@@ -0,0 +1,14 @@
import chalk from 'chalk';
import { execa } from 'execa';
export default async function validateYarn() {
try {
await execa('yarn', ['--version']);
} catch (err) {
console.log(
'%s Yarn is not installed, please install it before linking your extension',
chalk.red.bold('ERROR')
);
process.exit(1);
}
}

203
platform/cli/src/index.js Executable file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env node
import { Command } from 'commander';
import inquirer from 'inquirer';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { getPathQuestions, getRepoQuestions } from './questions.js';
import {
createPackage,
addExtension,
removeExtension,
addMode,
removeMode,
listPlugins,
searchPlugins,
linkExtension,
linkMode,
unlinkExtension,
unlinkMode,
} from './commands/index.js';
import chalk from 'chalk';
const runningDirectory = process.cwd();
const viewerDirectory = path.resolve(runningDirectory, 'platform/app');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJsonPath = path.join(runningDirectory, 'package.json');
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.name !== 'ohif-monorepo-root') {
console.log(packageJson);
console.log(chalk.red('ohif-cli must run from the root of the OHIF platform'));
process.exit(1);
}
} catch (error) {
console.log(chalk.red('ohif-cli must run from the root of the OHIF platform'));
process.exit(1);
}
function _createPackage(packageType) {
const pathQuestions = getPathQuestions(packageType);
const repoQuestions = getRepoQuestions(packageType);
let pathAnswers;
const askPathQuestions = () => {
inquirer.prompt(pathQuestions).then(answers => {
pathAnswers = answers;
if (pathAnswers.confirm) {
askRepoQuestions(answers.baseDir, answers.name);
} else {
askPathQuestions();
}
});
};
const askRepoQuestions = () => {
inquirer.prompt(repoQuestions).then(repoAnswers => {
const answers = {
...pathAnswers,
...repoAnswers,
};
const templateDir = path.join(__dirname, `../templates/${packageType}`);
answers.templateDir = templateDir;
answers.targetDir = path.join(answers.baseDir);
answers.packageType = packageType;
createPackage(answers);
});
};
askPathQuestions();
}
// for now ohif-cli is ran through yarn only.
// see ohif-cli.md section # OHIF Command Line Interface for reference.
const program = new Command('yarn run cli');
// Todo: inject with webpack
program
.version('2.0.7')
.description('OHIF CLI')
.configureHelp({ sortOptions: true, sortSubcommands: true })
.showHelpAfterError('(add --help for additional information)');
program
.command('create-extension')
.description('Create a new template Extension')
.action(() => {
_createPackage('extension');
});
program
.command('create-mode')
.description('Create a new template Mode')
.action(() => {
_createPackage('mode');
});
program
.command('add-extension <packageName> [version]')
.description('Adds an OHIF Extension')
.action((packageName, version) => {
// change directory to viewer
process.chdir(viewerDirectory);
addExtension(packageName, version);
});
program
.command('remove-extension <packageName>')
.description('Removes an OHIF Extension')
.action(packageName => {
// change directory to viewer
process.chdir(viewerDirectory);
removeExtension(packageName);
});
program
.command('add-mode <packageName> [version]')
.description('Add an OHIF Mode')
.action((packageName, version) => {
// change directory to viewer
process.chdir(viewerDirectory);
addMode(packageName, version);
});
program
.command('remove-mode <packageName>')
.description('Removes an OHIF Mode')
.action(packageName => {
// change directory to viewer
process.chdir(viewerDirectory);
removeMode(packageName);
});
program
.command('link-extension <packageDir>')
.description('Links a local OHIF Extension to the Viewer to be used for development')
.action(packageDir => {
if (!fs.existsSync(packageDir)) {
console.log(
chalk.red('The Extension directory does not exist, please provide a valid directory')
);
process.exit(1);
}
linkExtension(packageDir, { viewerDirectory });
});
program
.command('unlink-extension <extensionName>')
.description('Unlinks a local OHIF Extension from the Viewer')
.action(extensionName => {
unlinkExtension(extensionName, { viewerDirectory });
console.log(
chalk.green(
`Successfully unlinked Extension ${extensionName} from the Viewer, don't forget to run yarn install --force`
)
);
});
program
.command('link-mode <packageDir>')
.description('Links a local OHIF Mode to the Viewer to be used for development')
.action(packageDir => {
if (!fs.existsSync(packageDir)) {
console.log(chalk.red('The Mode directory does not exist, please provide a valid directory'));
process.exit(1);
}
linkMode(packageDir, { viewerDirectory });
});
program
.command('unlink-mode <modeName>')
.description('Unlinks a local OHIF Mode from the Viewer')
.action(modeName => {
unlinkMode(modeName, { viewerDirectory });
console.log(
chalk.green(
`Successfully unlinked Mode ${modeName} from the Viewer, don't forget to run yarn install --force`
)
);
});
program
.command('list')
.description('List Added Extensions and Modes')
.action(() => {
const configPath = path.resolve(viewerDirectory, './pluginConfig.json');
listPlugins(configPath);
});
program
.command('search')
.option('-v, --verbose', 'Verbose output')
.description('Search NPM for the list of Modes and Extensions')
.action(options => {
searchPlugins(options);
});
program.parse(process.argv);

View File

@@ -0,0 +1,95 @@
import path from 'path';
import os from 'os';
function getPathQuestions(packageType) {
return [
{
type: 'input',
name: 'name',
message: `What is the name of your ${packageType}?`,
validate: input => {
if (!input) {
return 'Please enter a name';
}
return true;
},
default: `my-${packageType}`,
},
{
type: 'input',
name: 'baseDir',
message: `What is the target path to create your ${packageType}?`,
suffix: `\n(we recommend you do not use the OHIF ${packageType} folder (./${packageType}s) unless you are developing a core ${packageType})`,
maxLength: 40,
validate: input => {
if (!input) {
console.log('Please provide a valid target directory path');
return;
}
return true;
},
filter: (input, answers) => {
// Replace ~ with the user's home directory
const expandedPath = input.replace(/^~(?=$|\/|\\)/, os.homedir());
// Resolve the path to an absolute path
const resolvedPath = path.resolve(expandedPath, answers.name);
return resolvedPath;
},
},
{
type: 'confirm',
name: 'confirm',
message: `Please confirm the above path for generating the ${packageType} folder:`,
},
];
}
function getRepoQuestions(packageType) {
return [
{
type: 'confirm',
name: 'gitRepository',
message: 'Should it be a git repository?',
default: false,
},
{
type: 'confirm',
name: 'prettier',
message: 'Should it follow same prettier rules as OHIF?',
},
{
type: 'input',
name: 'version',
message: `What is the version of your ${packageType}?`,
default: '0.0.1',
},
{
type: 'input',
name: 'description',
message: `What is the description of your ${packageType}?`,
default: '',
},
{
type: 'input',
name: 'author',
message: `Who is the author of your ${packageType}?`,
default: '',
},
{
type: 'input',
name: 'email',
message: 'What is your email address?',
default: '',
},
{
type: 'input',
name: 'license',
message: `What is the license of your ${packageType}?`,
default: 'MIT',
},
];
}
export { getPathQuestions, getRepoQuestions };

View File

@@ -0,0 +1,104 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5",
"printWidth": 100,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid",
"endOfLine": "auto"
}

View File

@@ -0,0 +1,96 @@
const path = require('path');
const pkg = require('../package.json');
const outputFile = 'index.umd.js';
const rootDir = path.resolve(__dirname, '../');
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
// Todo: add ESM build for the extension in addition to umd build
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
chunkFilename: '[name].chunk.js',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'@ohif/core': {
commonjs2: '@ohif/core',
commonjs: '@ohif/core',
amd: '@ohif/core',
root: '@ohif/core',
},
'@ohif/ui': {
commonjs2: '@ohif/ui',
commonjs: '@ohif/ui',
amd: '@ohif/ui',
root: '@ohif/ui',
},
},
],
module: {
rules: [
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
},
},
},
]
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
{
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
},
};
module.exports = config;

View File

@@ -0,0 +1,49 @@
module.exports = {
plugins: [
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};

View File

@@ -0,0 +1,62 @@
{
"repository": "OHIF/Viewers",
"keywords": ["ohif-extension"],
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:my-extension": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^{LATEST_OHIF_VERSION}",
"@ohif/extension-default": "^{LATEST_OHIF_VERSION}",
"@ohif/extension-cornerstone": "^{LATEST_OHIF_VERSION}",
"@ohif/i18n": "^1.0.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"babel-eslint": "9.x",
"babel-loader": "^8.2.4",
"@svgr/webpack": "^8.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^8.39.0",
"eslint-loader": "^2.0.0",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3",
"webpack-cli": "^5.0.2"
}
}

View File

@@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };

View File

@@ -0,0 +1,86 @@
import { id } from './id';
/**
* You can remove any of the following modules if you don't need them.
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* Perform any pre-registration tasks here. This is called before the extension
* is registered. Usually we run tasks such as: configuring the libraries
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
* this extension is providing.
*/
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {},
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ViewportModule should provide a list of viewports that will be available in OHIF
* for Modes to consume and use in the viewports. Each viewport is defined by
* {name, component} object. Example of a viewport module is the CornerstoneViewport
* that is provided by the Cornerstone extension in OHIF.
*/
getViewportModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ToolbarModule should provide a list of tool buttons that will be available in OHIF
* for Modes to consume and use in the toolbar. Each tool button is defined by
* {name, defaultComponent, clickHandler }. Examples include radioGroupIcons and
* splitButton toolButton that the default extension is providing.
*/
getToolbarModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* LayoutTemplateMOdule should provide a list of layout templates that will be
* available in OHIF for Modes to consume and use to layout the viewer.
* Each layout template is defined by a { name, id, component}. Examples include
* the default layout template provided by the default extension which renders
* a Header, left and right sidebars, and a viewport section in the middle
* of the viewer.
*/
getLayoutTemplateModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* SopClassHandlerModule should provide a list of sop class handlers that will be
* available in OHIF for Modes to consume and use to create displaySets from Series.
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
* Examples include the default sop class handler provided by the default extension
*/
getSopClassHandlerModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* HangingProtocolModule should provide a list of hanging protocols that will be
* available in OHIF for Modes to use to decide on the structure of the viewports
* and also the series that hung in the viewports. Each hanging protocol is defined by
* { name, protocols}. Examples include the default hanging protocol provided by
* the default extension that shows 2x2 viewports.
*/
getHangingProtocolModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* CommandsModule should provide a list of commands that will be available in OHIF
* for Modes to consume and use in the viewports. Each command is defined by
* an object of { actions, definitions, defaultContext } where actions is an
* object of functions, definitions is an object of available commands, their
* options, and defaultContext is the default context for the command to run against.
*/
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ContextModule should provide a list of context that will be available in OHIF
* and will be provided to the Modes. A context is a state that is shared OHIF.
* Context is defined by an object of { name, context, provider }. Examples include
* the measurementTracking context provided by the measurementTracking extension.
*/
getContextModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* DataSourceModule should provide a list of data sources to be used in OHIF.
* DataSources can be used to map the external data formats to the OHIF's
* native format. DataSources are defined by an object of { name, type, createDataSource }.
*/
getDataSourcesModule: ({ servicesManager, commandsManager, extensionManager }) => {},
};

104
platform/cli/templates/mode/.gitignore vendored Normal file
View File

@@ -0,0 +1,104 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5",
"printWidth": 100,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid",
"endOfLine": "auto"
}

View File

@@ -0,0 +1,100 @@
const path = require('path');
const pkg = require('../package.json');
const outputFile = 'index.umd.js';
const rootDir = path.resolve(__dirname, '../');
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
// Todo: add ESM build for the mode in addition to umd build
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
chunkFilename: '[name].chunk.js',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'@ohif/core': {
commonjs2: '@ohif/core',
commonjs: '@ohif/core',
amd: '@ohif/core',
root: '@ohif/core',
},
'@ohif/ui': {
commonjs2: '@ohif/ui',
commonjs: '@ohif/ui',
amd: '@ohif/ui',
root: '@ohif/ui',
},
'@ohif/mode-longitudinal': {
commonjs2: '@ohif/mode-longitudinal',
commonjs: '@ohif/mode-longitudinal',
amd: '@ohif/mode-longitudinal',
root: '@ohif/mode-longitudinal',
}
},
],
module: {
rules: [
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
},
},
},
]
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
{
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
},
};
module.exports = config;

View File

@@ -0,0 +1,43 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
'@babel/preset-typescript',
],
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};

View File

@@ -0,0 +1,53 @@
{
"repository": "OHIF/Viewers",
"keywords": [
"ohif-mode"
],
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "^{LATEST_OHIF_VERSION}"
},
"dependencies": {
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^8.0.0-beta.4",
"@svgr/webpack": "^8.1.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^8.39.0",
"eslint-loader": "^2.0.0",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3",
"webpack-cli": "^5.0.2"
}
}

View File

@@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };

View File

@@ -0,0 +1,140 @@
import { hotkeys } from '@ohif/core';
import { initToolGroups, toolbarButtons } from '@ohif/mode-longitudinal';
import { id } from './id';
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default',
leftPanel: '@ohif/extension-default.panelModule.seriesList',
rightPanel: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
};
const cornerstone = {
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
};
/**
* Just two dependencies to be able to render a viewport with panels in order
* to make sure that the mode is working.
*/
const extensionDependencies = {
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
};
function modeFactory({ modeConfiguration }) {
return {
/**
* Mode ID, which should be unique among modes used by the viewer. This ID
* is used to identify the mode in the viewer's state.
*/
id,
routeName: 'template',
/**
* Mode name, which is displayed in the viewer's UI in the workList, for the
* user to select the mode.
*/
displayName: 'Template Mode',
/**
* Runs when the Mode Route is mounted to the DOM. Usually used to initialize
* Services and other resources.
*/
onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => {
const { measurementService, toolbarService, toolGroupService } = servicesManager.services;
measurementService.clearMeasurements();
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
toolbarService.addButtons(toolbarButtons);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
]);
},
onModeExit: ({ servicesManager }: withAppTypes) => {
const {
toolGroupService,
syncGroupService,
segmentationService,
cornerstoneViewportService,
uiDialogService,
uiModalService,
} = servicesManager.services;
uiDialogService.dismissAll();
uiModalService.hide();
toolGroupService.destroy();
syncGroupService.destroy();
segmentationService.destroy();
cornerstoneViewportService.destroy();
},
/** */
validationTags: {
study: [],
series: [],
},
/**
* A boolean return value that indicates whether the mode is valid for the
* modalities of the selected studies. For instance a PET/CT mode should be
*/
isValidMode: ({ modalities }) => {
return { valid: true };
},
/**
* Mode Routes are used to define the mode's behavior. A list of Mode Route
* that includes the mode's path and the layout to be used. The layout will
* include the components that are used in the layout. For instance, if the
* default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout')
* it will include the leftPanels, rightPanels, and viewports. However, if
* you define another layoutTemplate that includes a Footer for instance,
* you should provide the Footer component here too. Note: We use Strings
* to reference the component's ID as they are registered in the internal
* ExtensionManager. The template for the string is:
* `${extensionId}.{moduleType}.${componentId}`.
*/
routes: [
{
path: 'template',
layoutTemplate: ({ location, servicesManager }) => {
return {
id: ohif.layout,
props: {
leftPanels: [ohif.leftPanel],
rightPanels: [ohif.rightPanel],
viewports: [
{
namespace: cornerstone.viewport,
displaySetsToDisplay: [ohif.sopClassHandler],
},
],
},
};
},
},
],
/** List of extensions that are used by the mode */
extensions: extensionDependencies,
/** HangingProtocol used by the mode */
// hangingProtocol: [''],
/** SopClassHandlers used by the mode */
sopClassHandlers: [ohif.sopClassHandler],
/** hotkeys for mode */
};
}
const mode = {
id,
modeFactory,
extensionDependencies,
};
export default mode;