Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create component command #234

Open
wants to merge 9 commits into
base: 2.x
Choose a base branch
from
104 changes: 104 additions & 0 deletions src/handlers/componentCreate.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like a test written for this if possible. I can help if needed!

Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import log from '../lib/log';
import {
EXIT_ERROR,
EMULSIFY_SYSTEM_CONFIG_FILE,
EMULSIFY_PROJECT_CONFIG_FILE,
} from '../lib/constants';
import type { EmulsifySystem } from '@emulsify-cli/config';
import type { CreateComponentHandlerOptions } from '@emulsify-cli/handlers';
import getGitRepoNameFromUrl from '../util/getGitRepoNameFromUrl';
import getEmulsifyConfig from '../util/project/getEmulsifyConfig';
import getJsonFromCachedFile from '../util/cache/getJsonFromCachedFile';
import cloneIntoCache from '../util/cache/cloneIntoCache';

import generateComponent from '../util/project/generateComponent';

/**
* Handler for the `component create` command.
*/
export default async function componentCreate(
name: string,
{ directory }: CreateComponentHandlerOptions
): Promise<void> {
const emulsifyConfig = await getEmulsifyConfig();
if (!emulsifyConfig) {
return log(
'error',
'No Emulsify project detected. You must run this command within an existing Emulsify project. For more information about creating Emulsify projects, run "emulsify init --help"',
EXIT_ERROR
);
}

// If there is no system or variant config, exit with a helpful message.
if (!emulsifyConfig.system || !emulsifyConfig.variant) {
return log(
'error',
'You must select and install a system before you can create components. To see a list of out-of-the-box systems, run "emulsify system list". You can install a system by running "emulsify system install [name]"',
EXIT_ERROR
);
}

// Parse the system name from the system repository path.
const systemName = getGitRepoNameFromUrl(emulsifyConfig.system.repository);
if (!systemName) {
return log(
'error',
`The system specified in your project configuration is not valid. Please make sure your ${EMULSIFY_PROJECT_CONFIG_FILE} file contains a system.repository value that is a valid git url`,
EXIT_ERROR
);
}

// Make sure the given system is installed and has the correct branch/commit/tag checked out.
try {
await cloneIntoCache('systems', [systemName])(emulsifyConfig.system);
} catch (e) {
return log(
'error',
'The system specified in your project configuration is not clone-able, or has an invalid checkout value.',
EXIT_ERROR
);
}

// Load the system configuration file.
const systemConf: EmulsifySystem | void = await getJsonFromCachedFile(
'systems',
[systemName],
emulsifyConfig.system.checkout,
EMULSIFY_SYSTEM_CONFIG_FILE
);

// If no systemConf is present, error with a helpful message.
if (!systemConf) {
return log(
'error',
`Unable to load configuration for the ${systemName} system. Please make sure the system is installed.`,
EXIT_ERROR
);
}

const variantName = emulsifyConfig.variant.platform;
const variantConf = systemConf.variants?.find(
({ platform }) => platform === variantName
);

if (!variantConf) {
return log(
'error',
`Unable to find configuration for the variant ${variantName} within the system ${systemName}.`,
EXIT_ERROR
);
}

if (!name) {
return log('error', 'Specify a name for the new component.');
}

try {
await generateComponent(variantConf, name, directory || '');
} catch (e) {
log(
'error',
`Unable to create the ${name} component: ${(e as Error).toString()}`
);
}
}
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import systemList from './handlers/systemList';
import systemInstall from './handlers/systemInstall';
import componentList from './handlers/componentList';
import componentInstall from './handlers/componentInstall';
import componentCreate from './handlers/componentCreate';

// Main program commands.
program
Expand Down Expand Up @@ -95,5 +96,16 @@ component
"Install a component from within the current project's system and variant"
)
.action(componentInstall);
component
.command('create [name]')
.option(
'-d --directory <directory>',
'Used to set the directory where the new component is to be created'
)
.alias('c')
.description(
"Create a component from within the current project's system and variant"
)
.action(componentCreate);

void program.parseAsync(process.argv);
4 changes: 4 additions & 0 deletions src/types/handlers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ declare module '@emulsify-cli/handlers' {
force?: boolean;
all?: boolean;
};

export type CreateComponentHandlerOptions = {
directory?: string | void;
};
}
145 changes: 145 additions & 0 deletions src/util/project/generateComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as fs from 'fs';
import { pathExists } from 'fs-extra';
import type { EmulsifyVariant } from '@emulsify-cli/config';
import { join, dirname } from 'path';
import { EMULSIFY_PROJECT_CONFIG_FILE } from '../../lib/constants';
import findFileInCurrentPath from '../fs/findFileInCurrentPath';
import log from '../../lib/log';

const storiesTemplate = (
componentName: string,
filename: string,
directory: string
) =>
`import ${componentName}Twig from './${filename}.twig';
import ${componentName}Data from './${filename}.yml';

/**
* Storybook Definition.
*/
export default { title: '${directory[0].toUpperCase() + directory.slice(1)}/${
componentName[0].toUpperCase() + componentName.slice(1)
}' };

export const ${componentName} = () => ${componentName}Twig(${componentName}Data);
`;

const twigTemplate = (filename: string, className: string) =>
`{#
/**
* Available variables:
* - ${filename}__heading - the content of the heading (typically text)
* - ${filename}__content - the content of the component (typically text)
*
* Available blocks:
* - ${filename}__content - used to replace the content of the button with something other than text
* for example: to insert an icon
*/
#}
{% set ${filename}__base_class = '${className}' %}

<div className="${filename}">
{{ ${filename}__heading }}
{% block ${filename}__content %}
{# Component content goes here #}
{{ ${filename}__content }}
{% endblock %}
</div>
`;

const scssTemplate = (className: string) =>
`.${className} {
// Your SCSS code goes here
}
`;

const ymlTemplate = (filename: string, componentName: string) =>
`${filename}__heading: '${componentName}'
${filename}__content: 'It is a descriptive text of the ${componentName} component'
`;

/**
* Installs a specified component within the Emulsify project the user is currently within.
*
* @param variant EmulsifyVariant object containing information about the component, where it lives, and how it should be created.
* @param componentName string name of the component that should be created.
* @param directory string name of the directory where it should be created.
* @returns
*/
export default async function generateComponent(
variant: EmulsifyVariant,
componentName: string,
directory: string
): Promise<void> {
// Gather information about the current Emulsify project. If none exists,
// throw an error.
const path = findFileInCurrentPath(EMULSIFY_PROJECT_CONFIG_FILE);
if (!path) {
throw new Error(
'Unable to find an Emulsify project to create the component into.'
);
}

// Find the component's parent structure within the given variant configuration. If the
// component's parent structure does not exist, throw an error.
const structure = variant.structureImplementations.find(
({ name }) => name === directory
);
if (!structure) {
throw new Error(
`The structure (${directory}) specified within the component ${componentName} is invalid.`
);
}

// Calculate the destination path based on the path to the Emulsify project, the structure of the
// component, and the component's name.
const destination = join(dirname(path), structure.directory, componentName);

// If the component already exists within the project,
// throw an error.
if (await pathExists(destination)) {
throw new Error(
`The component "${componentName}" already exists in ${structure.directory}`
);
}

const filename = componentName
.replace(/([a-z])([A-Z])/g, '$1_$2')
.toLowerCase();

const className = componentName
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase();

// Create the component directory
fs.mkdirSync(destination);

// Generate twig template file
const twigTemplateFile = twigTemplate(filename, className);
const twigTemplatePath = join(destination, `${filename}.twig`);
fs.writeFileSync(twigTemplatePath, twigTemplateFile);

// Generate yml template file
const ymlTemplateFile = ymlTemplate(filename, componentName);
const ymlTemplatePath = join(destination, `${filename}.yml`);
fs.writeFileSync(ymlTemplatePath, ymlTemplateFile);

// Generate scss template file
const scssTemplateFile = scssTemplate(className);
const scssTemplatePath = join(destination, `${filename}.scss`);
fs.writeFileSync(scssTemplatePath, scssTemplateFile);

// Generate stories template file
const storiesTemplateFile = storiesTemplate(
componentName,
filename,
directory
);
const storiesTemplatePath = join(destination, `${filename}.stories.js`);
fs.writeFileSync(storiesTemplatePath, storiesTemplateFile);

return log(
'success',
`The ${componentName} component has been created in ${structure.directory}`
);
}
Loading