import { createFilter } from '@rollup/pluginutils'; import { Plugin, SourceDescription } from 'rollup'; import { PackageJSON } from '../types/packages'; import resolve from 'resolve'; import pkgDir from 'pkg-dir'; import { readFileSync } from 'fs'; import YAML from 'js-yaml'; interface Options { output: string; outputJSON?: boolean; appManifest?: string; ignoreFrameworkVersionUpdate?: boolean; } interface ManifestPackageDep { name: string; version: string; } interface Manifest { name: string; package_manifests?: Manifest[]; integration_ids?: string[]; studio_options?: { framework_version?: string; package_dependencies?: ManifestPackageDep[]; }; } const APP_FILTER = createFilter(/app-manifest\.yml$/); const PACKAGE_FILTER = createFilter(/\.yml$/); export default function manifestMerger(opts: Options): Plugin { const fileName = opts.output; const outputJSON = !!opts.outputJSON; const shouldUpdateFrameworkVersion = !opts.ignoreFrameworkVersionUpdate; let appManifest: Manifest; const studioFrameworkPath = resolve.sync('@movable/studio-framework', { basedir: process.cwd() }); const frameworkPkgPath = `${pkgDir.sync(studioFrameworkPath)}/package.json`; const studioFrameworkPkg: PackageJSON = require(frameworkPkgPath); const pathPackageMap: Map = new Map(); const packageDependencies: ManifestPackageDep[] = [ { name: '@movable/studio-framework', version: studioFrameworkPkg.version } ]; const packageCache: Map = new Map(); return { name: 'Studio Manifest Merger Rollup Plugin', buildStart() { if (opts.appManifest) { this.addWatchFile(opts.appManifest); appManifest = YAML.load(readFileSync(opts.appManifest, 'utf-8')) as Manifest; } }, buildEnd(error) { if (error) { throw new Error(error.message); } if (!appManifest) { throw new Error( 'There was no app-manifest.yml imported. You may provide a "appManifest" to the "manifestBundler()" options' ); } }, resolveId(id: string): string | undefined { if (!APP_FILTER(id) && PACKAGE_FILTER(id)) { const path = resolve.sync(id, { basedir: process.cwd() }); const importRoot = pkgDir.sync(path); const pkg: PackageJSON = require(`${importRoot}/package.json`); pathPackageMap.set(path, pkg.name); this.addWatchFile(path); return path; } }, transform(yml: string, id: string): SourceDescription | null { const noCode: SourceDescription = { code: '', map: { mappings: '' } }; if (APP_FILTER(id)) { appManifest = YAML.load(yml) as Manifest; return noCode; } if (PACKAGE_FILTER(id)) { const packageManifest: Manifest = YAML.load(yml) as Manifest; const name = pathPackageMap.get(id); if (name) { packageManifest.name = name; } else { throw new Error( `Could not determine the package name for file: ${id}. Make sure that "nodeResolve()" is placed after "manifestMerger()"` ); } packageCache.set(id, packageManifest); return noCode; } return null; }, generateBundle() { // Make sure the studio_options key is present if (!appManifest.studio_options) appManifest.studio_options = {}; if (shouldUpdateFrameworkVersion) { // Add the framework version to studio_options appManifest.studio_options.framework_version = studioFrameworkPkg.version; appManifest.studio_options.package_dependencies = packageDependencies; } // Add the package manifests to the array const manifests: Manifest[] = []; const integrationIds = [...(appManifest.integration_ids || [])]; for (const [, val] of packageCache.entries()) { if (Array.isArray(val.integration_ids)) { integrationIds.push(...val.integration_ids); } manifests.push(val); } appManifest.package_manifests = manifests; if (integrationIds.length) { appManifest.integration_ids = [...new Set(integrationIds)]; } if (fileName) { const inline = 12; // The level where you switch to inline YAML const indent = 2; // The amount of spaces to use for indentation of nested nodes. const source = outputJSON ? JSON.stringify(appManifest) : YAML.dump(appManifest, { flowLevel: inline, indent: indent, forceQuotes: true }); // eslint-disable-next-line no-param-reassign this.emitFile({ type: 'asset', fileName, source }); } } }; }