feat(cli): Ohif cli for modes and extensions modification (#2696)

* feat: Add initial cli tool structure

* feat: add copying template files

* feat: Add mode template and command

* feat: Add readme template generation

* feat: Add documentation to extension template

* feat: Enhance documentation of the template mode

* fix: cli module type

* feat: Add config-based mode and extension registration (#2660)

* feat: Add ohif cli add/remove extension/mode (#2661)

* Basic working CLI for add-extension and remove-extension

* Basic cli for add/remove extension/mode, lots more to do.

* Cleanup and harden]

* feat: Add list of tasks to add-mode

Co-authored-by: Alireza <ar.sedghi@gmail.com>

* feat: Add git initialization for the mode or extension template (#2662)

* fix: package json file to include templates

* feat: Add git initialization for the mode or extension template

* feat: Add more checks of git and target dir

* feat: refactore library utilities

* feat: Add the list command to print extensions and modes (#2664)

* feat: Add the list command to print extensions and modes

* Add todo

* Feat/ohif cli validation + auto install (#2671)

* WIP

* Working mode keyword verification

* Validation

* auto install extensions based on modes

* WIP remove unused extensions on removeMove

* Working add-mode, remove-mode automatic extension management.

* If extension is in used by a mode, don't allow the CLI to uninstall it

* Cleanup addExtension

* cleanup removeExtension and addMode

* Cleanup removeMode

* Update existing extensions with the needed keywords/peer deps

* Fix broken config

* Feat/cli search (#2677)

* feat: refactor pretty print for console

* feat: add search for modes and extensions

* fix: ugly colors

* Feat/ohif cli error handling publishing (#2679)

* WIP

* fix: webpack imports

* wip

* fix: react router dom private routes

* from last commit

* wip

* fix: webpack prod builds

* WIP

* Working regsitration with new IDs

* Stable

Co-authored-by: Alireza <ar.sedghi@gmail.com>

* verify extensions when constructing modes. (#2681)

* verify extensions when constructing modes.

* Add version to unit tests so it conforms to schema

* Update ohif utils exposed via @ohif/core

* Fix import

* fix tests

* feat: ohif-cli link local modes/extensions for development (#2682)

* feat: enable cli to work with project root

* feat: add initial link package

* feat: add link and unlink extension

* feat: add link and unlink mode

* erro handling for link-package

* feat: add comment on ohif-cli linking for development (#2686)

* Docs/ohif cli (#2687)

* feat: Add documentation for templates

* feat: Add more documentation

* Fix/core publish (#2685)

* versions

* wip

* remove webpack clean output

* fix publish

* use next as dist tag for v3 for now

* fix webpack pro recipe for output

* fix: lerna publish next

* fix(cli): fix issues when trying to link an extension or a mode (#2725)

The generated package.json doesn't contain keywords property which is
required by the linkPackage function. The module apth wasn't correclty
handled too, and when there is no pluginOptions, it fails while reading
the file or while generating a default configuration.

* make dicom pdf and video work after cli merge

* add axios dependency

* comment out the chdir for now

* create id and version based on user inputs

* customizable path for extension and modes

* fix template to make the template mode load

* fix the questions to loop if path is not desirable

* fix templates

* correct package json order

* unify the package creation for extension and mode

* bump versions for each package

* bump extension versions to 3.0

* add gitignore to templates

* fix version requirements when ^

* update docs

* update docs and fix tests

* try to fix the tests

* bump node version

* remove the version from extensions

* remove the version from modes

* remove version from extensionManager

* fix eslint

* revert husky version

* fix eslint

* fix node version for new eslint

* fix documentatoin removing version

* fix cicle ci image version

* fix circle ci node image

* fix circle ci node image

* add back the video and pdf

Co-authored-by: Matthis Duclos <matthis.duclos@gmail.com>
Co-authored-by: James A. Petts <jamesapetts@gmail.com>
This commit is contained in:
authored and GitHub committed 2022-04-06 13:28:42 -04:00
1 parent 82df3eac03
commit 7f0ad6f84d
181 files changed
+5985 -3689

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
{
"name": "@ohif/cli",
"version": "2.0.7",
"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": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"cli",
"ohif"
],
"author": "OHIF Contributors",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.13.10",
"chalk": "^5.0.0",
"commander": "^8.3.0",
"axios": "^0.26.1",
"execa": "^6.0.0",
"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/"
]
}
+55
View File
@@ -0,0 +1,55 @@
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);
});
}
@@ -0,0 +1,38 @@
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);
});
}
+72
View File
@@ -0,0 +1,72 @@
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);
});
}
@@ -0,0 +1 @@
export default 'Not found';
@@ -0,0 +1,97 @@
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;
@@ -0,0 +1,8 @@
const colors = {
LIGHT: '#5acce6',
MAIN: '#0944b3',
DARK: '#090c29',
ACTIVE: '#348cfd',
};
export default colors;
@@ -0,0 +1,5 @@
const endPoints = {
NPM_KEYWORD: 'https://registry.npmjs.com/-/v1/search?text=keywords:',
};
export default endPoints;
+5
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 };
@@ -0,0 +1,6 @@
const keywords = {
MODE: 'ohif-mode',
EXTENSION: 'ohif-extension',
};
export default keywords;
+23
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,
};
+56
View File
@@ -0,0 +1,56 @@
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);
addToConfig(packageName, { version });
}
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 };
+23
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;
@@ -0,0 +1,49 @@
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);
});
}
@@ -0,0 +1,38 @@
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);
});
}
+68
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);
});
}
@@ -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;
@@ -0,0 +1,32 @@
import { execa } from 'execa';
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);
//update the plugin.json file
removeFromConfig(packageName);
};
function unlinkExtension(extensionName, options) {
linkPackage(extensionName, options, removeExtensionFromConfig);
}
function unlinkMode(modeName, options) {
linkPackage(modeName, options, removeModeFromConfig);
}
export { unlinkExtension, unlinkMode };
@@ -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 };
@@ -0,0 +1,42 @@
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;
@@ -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;
@@ -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;
@@ -0,0 +1,33 @@
import fs from 'fs';
import path from 'path';
async function editPackageJson(options) {
const { name, version, description, author, license, targetDir } = options;
// read package.json from targetDir
const dependenciesPath = path.join(targetDir, 'dependencies.json');
const rawData = fs.readFileSync(dependenciesPath, 'utf8');
const packageJson = JSON.parse(rawData);
// edit package.json
const mergedObj = Object.assign(
{
name,
version,
description,
author,
license,
files: ['dist', '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;
@@ -0,0 +1,67 @@
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;
}
@@ -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;
}
@@ -0,0 +1,3 @@
export default function getVersionedPackageName(packageName, version) {
return version === undefined ? packageName : `${packageName}@${version}`;
}
@@ -0,0 +1,5 @@
import { info } from 'yarn-programmatic';
export default async function getYarnInfo(packageName) {
return await info(packageName);
}
+50
View File
@@ -0,0 +1,50 @@
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,
};
@@ -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;
@@ -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;
@@ -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;
@@ -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,
};
}
@@ -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,
};
@@ -0,0 +1,40 @@
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,
};
@@ -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;
}
@@ -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,
{ flag: 'w+' },
(err) => {
if (err) {
console.error(err);
return;
}
}
);
}
@@ -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 };
@@ -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;
}
}
@@ -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;
+160
View File
@@ -0,0 +1,160 @@
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);
const response = await fetch(`${registryUrlOfPackage}${packageName}`);
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,
};
@@ -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);
}
}
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env node
import program 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/viewer');
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();
}
// Todo: inject with webpack
program.version('2.0.7').description('OHIF CLI');
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('Removes 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);
+85
View File
@@ -0,0 +1,85 @@
import path from 'path';
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} (we recommend you do not use the OHIF ${packageType} folder (./${packageType}s) unless you are developing a core ${packageType}):`,
validate: (input) => {
if (!input) {
console.log('Please provide a valid target directory path');
return;
}
return true;
},
filter: (input, answers) => {
return path.resolve(input, answers.name);
},
},
{
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?',
},
{
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 };
+104
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
@@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"printWidth": 80,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
@@ -0,0 +1,48 @@
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');
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'inline-source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
},
],
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx'],
},
};
module.exports = config;
@@ -0,0 +1,41 @@
module.exports = {
plugins: ['inline-react-svg', '@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-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',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};
@@ -0,0 +1,58 @@
{
"repository": "OHIF/Viewers",
"keywords": ["ohif-extension"],
"main": "dist/index.umd.js",
"module": "src/index.js",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
"dev:dicom-pdf": "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": "^3.0.0",
"@ohif/extension-default": "^1.0.1",
"@ohif/extension-cornerstone": "^3.0.0",
"@ohif/i18n": "^1.0.0",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^10.11.0",
"react-router": "^6.2.1",
"react-router-dom": "^6.2.1",
"webpack": "^5.50.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "7.7.6"
},
"devDependencies": {
"@babel/core": "^7.5.0",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@babel/plugin-transform-regenerator": "^7.4.5",
"@babel/plugin-transform-runtime": "^7.5.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^8.0.0-beta.4",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^5.0.1",
"eslint-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "^4.12.2",
"webpack-cli": "^3.0.8"
}
}
@@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };
@@ -0,0 +1,126 @@
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
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
+8
View File
@@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"printWidth": 80,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
@@ -0,0 +1,48 @@
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');
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'inline-source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
},
],
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx'],
},
};
module.exports = config;
@@ -0,0 +1,41 @@
module.exports = {
plugins: ['inline-react-svg', '@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-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',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};
@@ -0,0 +1,49 @@
{
"repository": "OHIF/Viewers",
"keywords": ["ohif-mode"],
"main": "dist/index.umd.js",
"module": "src/index.js",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --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": "^3.0.0"
},
"dependencies": {
"@babel/runtime": "7.7.6"
},
"devDependencies": {
"@babel/core": "^7.5.0",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@babel/plugin-transform-regenerator": "^7.4.5",
"@babel/plugin-transform-runtime": "^7.5.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^8.0.0-beta.4",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^5.0.1",
"eslint-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "^4.12.2",
"webpack-cli": "^3.0.8"
}
}
+5
View File
@@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };
+106
View File
@@ -0,0 +1,106 @@
import { id } from './id';
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default',
leftPanel: '@ohif/extension-default.panelModule.seriesList',
rightPanel: '@ohif/extension-default.panelModule.measure',
};
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 }) => {},
/**
* Runs when the Mode Route is unmounted from the DOM. Usually used to clean
* up resources and states
*/
onModeExit: () => {},
/** */
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 }) => 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,
/** HangingProtocols used by the mode */
hangingProtocols: [''],
/** SopClassHandlers used by the mode */
sopClassHandlers: [ohif.sopClassHandler],
/** hotkeys for mode */
hotkeys: [''],
};
}
const mode = {
id,
modeFactory,
extensionDependencies,
};
export default mode;