UNPKG

@bolt/build-utils

Version:

Build-related utilities and helper scripts used in the Bolt Design System

316 lines (281 loc) 10.5 kB
/* eslint-disable no-await-in-loop */ const { promisify } = require('util'); const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); const log = require('./log'); const writeFile = promisify(fs.writeFile); const { getConfig } = require('./config-store'); let config; // cached Bolt config const { aggregateBoltDependencies } = require('./manifest.deprecated'); const getPkgInfo = require('./manifest.get-pkg-info'); // utility function to help with removing duplicate objects (like shared dependencies, extra Sass files included more than once, etc) function deduplicateObjectsInArray(arr, key) { return [...new Map(arr.map(item => [item[key], item])).values()]; } let boltManifest = { name: 'Bolt Manifest', version: '', // retrieved below components: { global: [], globalDeps: [], // global Bolt dependencies aggregated from package.json individual: [], }, }; // getting `boltManifest.version` // ideally we want the version from `lerna.json` as that's always the highest, but sometimes that file is not located at `../../../lerna.json` - like when this is compiling in a Drupal Site (Drupal Lab doesn't count), in that case we'll just fall back on the version from this package. try { boltManifest.version = JSON.parse( fs.readFileSync(path.join(__dirname, '../../../lerna.json'), 'utf8'), ).version; } catch (error) { boltManifest.version = JSON.parse( fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'), ).version; } // let missingBoltPkgs = []; let explicitBoltDependencies = []; async function buildBoltManifest() { config = config || (await getConfig()); let missingBoltPkgs = []; try { if (config.components.global) { // process through Bolt packages explicitly listed in the user's .boltrc config explicitBoltDependencies = await Promise.all( config.components.global.map(async item => { const { processedPkg, missingPkgs } = await getPkgInfo(item); missingBoltPkgs = [...missingBoltPkgs, ...missingPkgs]; return processedPkg; }), ); // combine both sets of explicit and implicit dependencies and deduplicate boltManifest.components.global = deduplicateObjectsInArray( [...explicitBoltDependencies], 'name', ); } if (config.components.individual) { const explicitIndividualBoltDependencies = await Promise.all( config.components.individual.map(async item => { const { processedPkg, missingPkgs } = await getPkgInfo(item); missingBoltPkgs = [...missingBoltPkgs, ...missingPkgs]; return processedPkg; }), ); boltManifest.components.individual = deduplicateObjectsInArray( [...explicitIndividualBoltDependencies], 'name', ); } if (missingBoltPkgs.length > 0) { boltManifest.components.global = await aggregateBoltDependencies( explicitBoltDependencies, ); } if (missingBoltPkgs.length > 0 && !hasWarnedAboutMissingPkgs) { missingBoltPkgs.flatMap(function callback(item) { return item.name; }); // Note: commenting out the warning below because it's no longer relevant. We will continue to automatically include any missing Bolt dependencies rather than rework this part of the build. // console.warn( // chalk.keyword('orange')( // `\nWarning! Some of the components being compiled have Bolt dependencies and/or peerDependencies that appear to be missing from your build configuration. \n\nTo prep for the upcoming Bolt v3.0 release, you'll need to add these packages to the `, // ) + // chalk.keyword('white')(`components: { global: [] } `) + // chalk.keyword('orange')(`section in your `) + // chalk.keyword('white')(`${config.configFileUsed} `) + // chalk.keyword('orange')( // `file to ensure that Twig namespaces are properly registered, any related Sass and JS code is compiled and loaded, and to have the fastest front-end web performance.\n`, // ), // ); // console.warn(missingBoltPkgs); // console.log('\n'); hasWarnedAboutMissingPkgs = true; } } catch (err) { log.errorAndExit('Error building Bolt Manifest', err); } return boltManifest; } let hasWarnedAboutMissingPkgs = false; async function getBoltManifest() { return await buildBoltManifest(); } /** * Get all directories for components in Bolt Manifest * @param relativeFrom {string} - If present, the path will be relative from this, else it will be absolute. * @returns {Array<String>} {dirs} - List of all component/package paths in Bolt Manifest */ async function getAllDirs(relativeFrom) { const dirs = []; const manifest = await getBoltManifest(); [manifest.components.global, manifest.components.individual].forEach( componentList => { componentList.forEach(component => { dirs.push( relativeFrom ? path.relative(relativeFrom, component.dir) : component.dir, ); }); }, ); return dirs; } /** * Similar to createComponentsManifest, this function provides an object of Twig namespaces that map 1:1 with the * cooresponding NPM package name. */ async function mapComponentNameToTwigNamespace() { const twigNamespaces = {}; const manifest = await getBoltManifest(); const allComponents = [ ...manifest.components.global, ...manifest.components.individual, ]; allComponents.forEach(component => { if (component.twigNamespace) { twigNamespaces[component.twigNamespace] = component.name; } }); return twigNamespaces; } async function createComponentsManifest() { const components = {}; const manifest = await getBoltManifest(); const allComponents = [ ...manifest.components.global, ...manifest.components.individual, ]; allComponents.forEach(component => { if (component.twigNamespace) { components[component.twigNamespace] = component; } }); return components; } async function writeBoltManifest() { const config = await getConfig(); try { await writeFile( path.resolve(config.dataDir, './full-manifest.bolt.json'), JSON.stringify(await getBoltManifest()), ); await writeFile( path.resolve(config.dataDir, './components.bolt.json'), JSON.stringify(await createComponentsManifest()), ); await writeFile( path.resolve(config.dataDir, './config.bolt.json'), JSON.stringify(config), ); } catch (error) { log.errorAndExit('Could not write bolt manifest files', error); } } /** * Builds config for Twig Namespaces * @param relativeFrom {string} - If present, the path will be relative from this, else it will be absolute. * @param extraNamespaces {object} - Extra namespaces to add to file in [this format](https://packagist.org/packages/evanlovely/plugin-twig-namespaces) * @async * @see writeTwigNamespaceFile * @returns {Promise<object>} */ async function getTwigNamespaceConfig(relativeFrom, extraNamespaces = {}) { const config = await getConfig(); const namespaces = {}; const allDirs = []; const manifest = await getBoltManifest(); const global = manifest.components.global; const individual = manifest.components.individual; [global, individual].forEach(componentList => { componentList.forEach(component => { if (!component.twigNamespace) { return; } const dir = relativeFrom ? path.relative(relativeFrom, component.dir) : component.dir; namespaces[component.basicName] = { recursive: true, paths: [dir], }; allDirs.push(dir); }); }); const namespaceConfigFile = Object.assign( { // Can hit anything with `@bolt` bolt: { recursive: true, paths: [...allDirs], }, 'bolt-data': { recursive: true, paths: [config.dataDir], }, 'bolt-assets': { recursive: true, paths: [config.buildDir], }, }, namespaces, ); // `extraNamespaces` serves two purposes: // 1. To add extra namespaces that have not been declared // 2. To add extra paths to previously declared namespaces // Assuming we've already declared the `foo` namespaces to look in `~/my-dir1` // Then someone uses `extraNamespaces` to declare that `foo` will look in `~/my-dir2` // This will not overwrite it, but *prepend* to the paths, resulting in a namespace setting like this: // 'foo': { // paths: ['~/my-dir2', '~/my-dir1'] // } // This causes the folder declared in `extraNamespaces` to be looked in first for templates, before our default; // allowing end user developers to selectively overwrite some templates. if (extraNamespaces) { Object.keys(extraNamespaces).forEach(namespace => { const settings = extraNamespaces[namespace]; if ( namespaceConfigFile[namespace] && settings.paths !== undefined // make sure the paths config is defined before trying to merge ) { // merging the two, making sure the paths from `extraNamespaces` go first namespaceConfigFile[namespace].paths = [ ...settings.paths, ...namespaceConfigFile[namespace].paths, ]; // don't add a new namespace key if the paths config option wasn't defined. prevents PHP errors if a namespace key was defined but no paths specified. } else if (settings.paths !== undefined) { namespaceConfigFile[namespace] = settings; } }); } return namespaceConfigFile; } /** * Write Twig Namespace File * Creates `bolt-twig-namespaces.json` in `config.dataDir` from the Bolt Manifest. That is pulled in by [Twig Namespace plugin](https://packagist.org/packages/evanlovely/plugin-twig-namespaces) in the PL config file. * @see getTwigNamespaceConfig; * @return {Promise<void>} */ async function writeTwigNamespaceFile() { const config = await getConfig(); const namespaceConfigFile = await getTwigNamespaceConfig( process.cwd(), config.extraTwigNamespaces, ); await writeFile( path.join(config.dataDir, 'twig-namespaces.bolt.json'), JSON.stringify(namespaceConfigFile, null, ' '), ); } module.exports = { buildBoltManifest, getBoltManifest, writeBoltManifest, writeTwigNamespaceFile, getTwigNamespaceConfig, mapComponentNameToTwigNamespace, getAllDirs, getPkgInfo, };