generator-mc-d8-theme
Version:
Yeoman generator for creating Drupal 8 component based themes.
287 lines (268 loc) ⢠10 kB
JavaScript
const Generator = require('yeoman-generator');
const _ = require('lodash');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const jsYaml = require('js-yaml');
const assert = require('assert');
const replace = require('replace-in-file');
// Helper to generate component libraries.
const buildComponents = require('./build-components');
// Helper to add in third party dependencies.
const addThirdParty = require('./add-third-party');
module.exports = class extends Generator {
constructor(args, options) {
super(args, options);
// Allow the theme generator main app to pass through the machine name.
// --theme-name=hey_yall
this.option('theme-name', {
type: String,
desc: 'The theme machine name'
});
}
initializing() {
// Grab the theme machine name if it's passed in.
const themeName = this.options.themeName || '';
this.themeNameMachine = _.snakeCase(themeName);
}
prompting() {
const prompts = [
{
type: 'checkbox',
name: 'howMuchTheme',
message: 'Which starter components do you want to add to your theme?',
// Be nice for these to be populated from an external repo
// and use a package.json to build this list.
// Or just do it based on folder name. /shrug
choices: [
{
value: 'accordion',
name: 'Accordion'
},
{
value: 'button',
name: 'Button'
},
{
value: 'card',
name: 'Card (Depends on Eyebrow and Heading components.)'
},
{
value: 'card-list',
name: 'Card List (Depends on Card, Eyebrow and Heading components.)'
},
{
value: 'carousel',
name: 'Carousel (Depends on Heading, Media and Button components.)'
},
{
value: 'eyebrow',
name: 'Eyebrow'
},
{
value: 'heading',
name: 'Heading'
},
{
value: 'hero',
name: 'Hero (Depends on Heading, Media and Button components.)'
},
{
value: 'media-item',
name: 'Media'
},
{
value: 'message',
name: 'Drupal Messages'
},
{
value: 'tabs',
name: 'Drupal Tabs'
}
]
}
];
// If there's no theme machine name provided, prompt the user for it.
if (!this.themeNameMachine) {
let defaultThemeName = '';
try {
// See if package.json exists.
fs.accessSync(this.destinationPath('package.json'), fs.constants.R_OK);
// If it does, read it and use the name as our default
// theme machine name.
const pkg = JSON.parse(
fs.readFileSync(
path.resolve(this.destinationPath('package.json')), 'utf8'
)
);
defaultThemeName = pkg.name;
}
catch (err) {
assert.fail(
`
šØ ${chalk.red(this.destinationPath('package.json'))} ${chalk.red('is missing')}.
${chalk.blue('Make sure you\'re running this command from your theme root.')}`
);
}
prompts.push({
name: 'themeNameMachine',
message: 'What is your theme\'s machine name? EX: unicorn_theme',
default: defaultThemeName
});
}
return this.prompt(prompts).then(function (props) {
// Try to use the name passed in via option else use
// the user provided theme machine name.
this.themeNameMachine = this.themeNameMachine || props.themeNameMachine;
// Check to see if any of the components that need dependencies
// are selected.
// card requires eyebrow, heading
if (props.howMuchTheme.includes('card')) {
props.howMuchTheme.push('eyebrow', 'heading');
}
// card-list requires card, eyebrow, heading
if (props.howMuchTheme.includes('card-list')) {
props.howMuchTheme.push('card', 'eyebrow', 'heading');
}
// carousel OR hero requires heading, media, button
if (
props.howMuchTheme.includes('carousel') ||
props.howMuchTheme.includes('hero')
) {
props.howMuchTheme.push('heading', 'media-item', 'button');
}
// props.howMuchTheme is an array of all selected options.
// i.e. [ 'hero', 'tabs', 'messages' ]
// Remove any duplicate components using uniq().
this.exampleComponents = _.uniq(props.howMuchTheme);
// Filter out any components that already exist within
// an existing theme.
try {
// Read the theme libraries.yml file to see which components
// already exist.
const librariesFile = jsYaml.safeLoad(
fs.readFileSync(
this.destinationPath(`${this.themeNameMachine}.libraries.yml`),
'utf8'
)
);
const existingLibraries = Object.keys(librariesFile);
// Exclude any components that already exist in the libraries file.
this.exampleComponents = _.difference(
this.exampleComponents,
existingLibraries
);
}
catch (e) {
// No libraries file found but that's ok. It won't be found unless
// this is run from an existing theme.
}
// To access props later use this.props.someAnswer;
this.props = props;
}.bind(this));
}
writing() {
// If any example components were selected...
if (this.exampleComponents.length > 0) {
// ...copy over the example components.
buildComponents({
exampleComponents: this.exampleComponents,
app: this
})
.then(buildComponentsConfig => {
// Add in any third party dependencies before we write
// to the libraries.yml file.
buildComponentsConfig = addThirdParty(buildComponentsConfig);
// Loop through the different components and append them to the
// libraries.yml file.
buildComponentsConfig.forEach((component) => {
// This is a little weird:
// 1. If this is being run from the parent generator we need to use
// this.fs.append() since we're copying the original libraries.yml
// template. If we just use fs.appendFileSync() there
// will be a conflict.
// 2. However if this is being run as a standalone sub generator
// this has to use this.appendFileSync() because otherwise it tries
// to overwrite the existing libraries file.
// Find out if this was called via the parent generator:
if (this.options.themeName) {
this.fs.append(
this.destinationPath(this.themeNameMachine + '.libraries.yml'),
jsYaml.safeDump(component),
{
trimEnd: false,
separator: '\r\n'
}
);
}
else {
// Add a blank line so the file is nicely formatted and the
// appended data doesn't run into the current data within
// the file.
fs.appendFileSync(
this.destinationPath(this.themeNameMachine + '.libraries.yml'),
'\r\n'
);
// Update the libraries.yml file with the new component library.
fs.appendFileSync(
this.destinationPath(this.themeNameMachine + '.libraries.yml'),
jsYaml.safeDump(component),
(err) => {
if (err) {
this.log(
chalk.red(
`Failed to update ${this.themeNameMachine}.libraries.yml`
)
);
}
}
);
}
});
})
.catch(error => {
// eslint-disable-next-line no-console
console.error(error);
});
}
}
install() {
// If `carousel` is selected, attempt to link up the slick
// carousel dependency. It'll still be up to the user to add SlickJS
// as a project dependency.
if (this.exampleComponents.indexOf('carousel') !== -1) {
// If a carousel third party library is required, add it to Pattern Lab
// so it works there.
replace({
files: this.destinationPath('src/styleguide/meta/_00-head.twig'),
from: /<!-- Vendor CSS placeholder -->/g,
to: '<link rel="stylesheet" href="/libraries/slick-carousel/slick/slick.css" media="all" />'
})
.catch(() => {
this.log('Failed to append slick css to Pattern Lab file styleguide/meta/_00-head.twig');
});
replace({
files: this.destinationPath('src/styleguide/meta/_01-foot.twig'),
from: /<!-- Vendor JS placeholder -->/g,
to: '<script src="/libraries/slick-carousel/slick/slick.min.js"></script>'
})
.catch(() => {
this.log('Failed to append slick js to Pattern Lab file styleguide/meta/_01-foot.twig');
});
}
}
end() {
// If `carousel` is selected, inform the user that they need to install
// the SlickJS carousel dependency.
if (this.exampleComponents.indexOf('carousel') !== -1) {
this.log('------------------------------------------------------------');
this.log('š You installed the Carousel component which requires SlickJS.');
this.log('Install SlickJS using composer with:');
this.log('composer require npm-asset/slick-carousel --working-dir=../../../../');
this.log('OR');
this.log(`Manually add Slick to the /libraries folder and update the carousel library in the ${this.themeNameMachine}.libraries.yml file.`);
this.log('https://github.com/kenwheeler/slick/');
this.log('------------------------------------------------------------');
}
}
};