const pluginConfig = require('../pluginConfig.json'); const fs = require('fs'); const os = require('os'); const path = require('path'); const autogenerationDisclaimer = ` // THIS FILE IS AUTOGENERATED AS PART OF THE EXTENSION AND MODE PLUGIN PROCESS. // IT SHOULD NOT BE MODIFIED MANUALLY \n`; const extractName = val => (typeof val === 'string' ? val : val.packageName); const publicURL = process.env.PUBLIC_URL || '/'; function isAbsolutePath(path) { return path.startsWith('http') || path.startsWith('/'); } function constructLines(input, categoryName) { let pluginCount = 0; const lines = { importLines: [], addToWindowLines: [], }; if (!input) return lines; input.forEach(entry => { if (entry.default === false) return; const packageName = extractName(entry); lines.addToWindowLines.push(`${categoryName}.push("${packageName}");\n`); pluginCount++; }); return lines; } function getFormattedImportBlock(importLines) { let content = ''; // Imports importLines.forEach(importLine => { content += importLine; }); return content; } function getFormattedWindowBlock(addToWindowLines) { let content = 'const extensions = [];\n' + 'const modes = [];\n' + '\n// Not required any longer\n' + 'window.extensions = extensions;\n' + 'window.modes = modes;\n\n'; addToWindowLines.forEach(addToWindowLine => { content += addToWindowLine; }); return content; } function getRuntimeLoadModesExtensions(modules) { const dynamicLoad = []; dynamicLoad.push( '\n\n// Add a dynamic runtime loader', 'async function loadModule(module) {', " if (typeof module !== 'string') return module;" ); modules.forEach(module => { const packageName = extractName(module); if (!packageName) { return; } if (module.importPath) { dynamicLoad.push( ` if( module==="${packageName}") {`, ` const imported = await window.browserImportFunction('${isAbsolutePath(module.importPath) ? '' : publicURL}${module.importPath}');`, ' return ' + (module.globalName ? `window["${module.globalName}"];` : `imported["${module.importName || 'default'}"];`), ' }' ); return; } dynamicLoad.push( ` if( module==="${packageName}") {`, ` const imported = await import("${packageName}");`, ' return imported.default;', ' }' ); }); // TODO - handle more cases for import than just default dynamicLoad.push( ' return (await window.browserImportFunction(module)).default;', '}\n', '// Import a list of items (modules or string names)', '// @return a Promise evaluating to a list of modules', 'export default function importItems(modules) {', ' return Promise.all(modules.map(loadModule));', '}\n', 'export { loadModule, modes, extensions, importItems };\n\n' ); return dynamicLoad.join('\n'); } const fromDirectory = (srcDir, dirPath) => { if (!dirPath) return; if (dirPath[0] === '.') return srcDir + '/../../..' + dirPath.substring(1); if (dirPath[0] === '~') return os.homedir() + dirPath.substring(1); return dirPath; }; const APP_SRC_DIR = path.resolve(__dirname, '../src'); const REPO_ROOT = path.resolve(__dirname, '../../../'); // The set of plugin package names declared in pluginConfig.json. Resolution and // asset copying are driven entirely by this list — a package present in the // extensions/ or modes/ workspaces but NOT listed here is ignored, and an // external (out-of-tree) package listed here with a `directory` is included. let declaredPluginNamesCache; function getDeclaredPluginNames() { if (declaredPluginNamesCache) { return declaredPluginNamesCache; } const names = new Set(); for (const entry of [...(pluginConfig.extensions || []), ...(pluginConfig.modes || [])]) { const name = extractName(entry); if (name) { names.add(name); } } declaredPluginNamesCache = names; return names; } // Map each in-tree plugin's real package name to its directory, but ONLY for the // plugins declared in pluginConfig.json. This lets the bundler resolve those // plugins from their source without them being dependencies of platform/app // (and therefore without entries in package.json / the lockfile), while leaving // undeclared workspace packages out of the build entirely. let workspacePluginDirsCache; function getWorkspacePluginDirs() { if (workspacePluginDirsCache) { return workspacePluginDirsCache; } const declared = getDeclaredPluginNames(); const map = {}; for (const group of ['extensions', 'modes']) { const root = path.join(REPO_ROOT, group); if (!fs.existsSync(root)) { continue; } for (const dir of fs.readdirSync(root)) { const pkgJsonPath = path.join(root, dir, 'package.json'); if (!fs.existsSync(pkgJsonPath)) { continue; } try { const { name } = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); if (name && declared.has(name)) { map[name] = path.join(root, dir); } } catch { // ignore an unparseable package.json } } } workspacePluginDirsCache = map; return map; } // Source directory of a workspace plugin: an explicit `directory` override wins // (out-of-tree plugins), otherwise look it up among the in-tree workspaces by // package name. Returns undefined for an external plugin that is instead // installed as a normal dependency (resolved from node_modules — see // pluginAssetDir and getPluginResolveAliases). function workspacePluginDir(plugin) { if (plugin.directory) { return fromDirectory(APP_SRC_DIR, plugin.directory); } return getWorkspacePluginDirs()[extractName(plugin)]; } // Where a plugin's copyable assets (public/, dist/) live. In-tree and // `directory`-overridden plugins use their source dir; anything else declared // in pluginConfig falls back to node_modules. This is what lets an external // extension/mode be included by adding it to the root package.json as a normal // dependency (e.g. third-party packages such as dicom-microscopy-viewer). function pluginAssetDir(plugin) { const dir = workspacePluginDir(plugin); if (dir) { return dir; } const name = extractName(plugin); const inNodeModules = name && path.join(REPO_ROOT, 'node_modules', name); return inNodeModules && fs.existsSync(inNodeModules) ? inNodeModules : undefined; } // Alias map fed into webpack `resolve.alias`. The trailing `$` makes each alias // an EXACT match for the bare package specifier that the generated // pluginImports.js imports, so deep subpath imports (e.g. // `@ohif/extension-cornerstone/types`) still flow through normal resolution and // honor each package's `exports` map. // // Only in-tree / `directory`-overridden plugins get an alias. An external // plugin installed as a root dependency intentionally gets none: its bare // specifier then resolves through webpack's normal node_modules walk-up // (resolve.modules includes the repo-root node_modules), exactly like any other // installed package. function getPluginResolveAliases() { const alias = {}; for (const entry of [...(pluginConfig.extensions || []), ...(pluginConfig.modes || [])]) { const name = extractName(entry); const dir = workspacePluginDir(entry); if (name && dir) { alias[`${name}$`] = dir; } } return alias; } // Build CopyPlugin patterns for a set of plugins. // // For `public`-section entries (literalDirectory=true) a `directory` is the // asset source itself — e.g. `./platform/public` or // dicom-microscopy-viewer's prebuilt dist folder — so it is copied directly. // // For extension/mode entries a `directory` is instead the package ROOT (it // doubles as the resolve alias target), so we copy its (public/ // or dist/) subdirectory, exactly as we do for in-tree and node_modules // plugins. This keeps an out-of-tree extension's assets landing in the same // place as an in-tree one. const createCopyPluginToDist = (distDir, plugins, folderName, { literalDirectory = false } = {}) => { return plugins .map(plugin => { let from; if (literalDirectory && plugin.directory) { from = fromDirectory(APP_SRC_DIR, plugin.directory); } else { const dir = pluginAssetDir(plugin); from = dir && path.join(dir, folderName); } return from && fs.existsSync(from) ? { from, to: `${distDir}${plugin.to || ''}`, toType: 'dir', } : undefined; }) .filter(Boolean); }; function writePluginImportsFile(SRC_DIR, DIST_DIR) { let pluginImportsJsContent = autogenerationDisclaimer; const extensionLines = constructLines(pluginConfig.extensions, 'extensions'); const modeLines = constructLines(pluginConfig.modes, 'modes'); pluginImportsJsContent += getFormattedImportBlock([ ...extensionLines.importLines, ...modeLines.importLines, ]); pluginImportsJsContent += getFormattedWindowBlock([ ...extensionLines.addToWindowLines, ...modeLines.addToWindowLines, ]); pluginImportsJsContent += getRuntimeLoadModesExtensions([ ...pluginConfig.extensions, ...pluginConfig.modes, ...pluginConfig.public, ]); fs.writeFileSync(`${SRC_DIR}/pluginImports.js`, pluginImportsJsContent, { flag: 'w+' }, err => { if (err) { console.error(err); return; } }); // Copy each extension/mode's static `public/` assets into the app dist. // Plugins are resolved from their source dir (see pluginAssetDir), so this // works whether they are in-tree, out-of-tree (`directory`), or installed as // dependencies of platform/app. const copyPluginPublicToDist = createCopyPluginToDist( DIST_DIR, [...pluginConfig.modes, ...pluginConfig.extensions], 'public' ); // Some extensions/modes ship prebuilt chunks/workers/wasm in dist/; copy them // if present. const copyPluginDistToDist = createCopyPluginToDist( DIST_DIR, [...pluginConfig.modes, ...pluginConfig.extensions], 'dist' ); // `public`-section entries (e.g. ./platform/public, dicom-microscopy-viewer) // point `directory` at the asset folder itself, so copy it verbatim. const copyPublicSectionToDist = createCopyPluginToDist( DIST_DIR, pluginConfig.public || [], 'public', { literalDirectory: true } ); return [...copyPluginPublicToDist, ...copyPluginDistToDist, ...copyPublicSectionToDist]; } module.exports = writePluginImportsFile; module.exports.getPluginResolveAliases = getPluginResolveAliases;