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

LG-3534 Create sub component tool #1945

Merged
merged 9 commits into from
Aug 30, 2023
6 changes: 6 additions & 0 deletions .changeset/ninety-hats-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@lg-tools/create': minor
'@lg-tools/cli': minor
---

Adds `--parent` flag to `lg create` command. Passing in this flag will create a subcomponent of the given parent.
4 changes: 4 additions & 0 deletions tools/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ cli
'-d, --directory [directory]',
'The directory to write the new package. Defaults to the first entry in lg.config.json scopes',
)
.option(
'-p, --parent [parent]',
'Creates a sub-component to the provided parent',
)
.action(createPackage);

/** Install */
Expand Down
1 change: 1 addition & 0 deletions tools/create/src/create.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface CreatePackageOptions {
directory?: string;
scope?: string;
parent?: string;
}

export interface TemplateParameters {
Expand Down
107 changes: 107 additions & 0 deletions tools/create/src/createComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import fse from 'fs-extra';
import path from 'path';

import { getNameVariants } from './utils/getNameVariants';
import { writeFiles } from './utils/writeFiles';
import { CreatePackageOptions } from './create.types';
import {
component,
componentIndex,
index,
pkgJson,
readMe,
spec,
story,
styles,
tsConfig,
types,
} from './templates';

interface CreateComponentArgs
extends Pick<Required<CreatePackageOptions>, 'scope' | 'directory'> {
name: string;
}

export function createComponent({
name,
scope,
directory,
}: CreateComponentArgs) {
const rootDir = process.cwd();

const { packageNameKebab, packageNameTitle, packageNamePascal } =
getNameVariants(name);

console.log(
chalk.green(
`Creating package ${chalk.bold(scope + '/' + packageNameKebab)}`,
),
);

// Create the appropriate directory
const packageDir = path.resolve(rootDir, directory, packageNameKebab);

// Create the package directories
fse.mkdir(packageDir, { recursive: true }, err => {
if (err) {
console.log(`Package ${packageNameKebab} already exists`);
return;
}

// Write the appropriate files for each template
writeFiles(
[
{
name: 'package.json',
contents: pkgJson({
scope,
packageNameKebab,
packageNameTitle,
}),
},
{
name: 'tsconfig.json',
contents: tsConfig,
},
{
name: 'README.md',
contents: readMe({ packageNameKebab, packageNameTitle }),
},
{
name: 'src/index.ts',
contents: index({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}.stories.tsx`,
contents: story({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/index.ts`,
contents: componentIndex({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.tsx`,
contents: component({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.spec.tsx`,
contents: spec({ packageNamePascal, packageNameKebab }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.types.ts`,
contents: types({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.styles.ts`,
contents: styles,
},
],
{
dir: packageDir,
packageNamePascal,
},
);
});
}
73 changes: 73 additions & 0 deletions tools/create/src/createSubComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import fse from 'fs-extra';
import path from 'path';

import { getComponentPath } from './utils/getComponentPath';
import { getNameVariants } from './utils/getNameVariants';
import { writeFiles } from './utils/writeFiles';
import { CreatePackageOptions } from './create.types';
import { component, componentIndex, spec, styles, types } from './templates';

interface CreateComponentArgs {
name: string;
parent: Required<CreatePackageOptions>['parent'];
}

export function createSubComponent({ name, parent }: CreateComponentArgs) {
const { packageNamePascal: parentNamePascal } = getNameVariants(parent);
const { packageNameKebab, packageNamePascal } = getNameVariants(name);

console.log(
chalk.green(
`Creating sub-component ${chalk.bold(
parentNamePascal + '/' + packageNamePascal,
)}`,
),
);

const parentDir = getComponentPath(parent);

if (!parentDir) {
console.error(chalk.red(`Could not find parent package ${parent}`));
process.exit(127);
}

const subComponentDir = path.resolve(parentDir, 'src', packageNamePascal);

fse.mkdir(subComponentDir, { recursive: true }, err => {
if (err) {
console.log(`Package ${packageNameKebab} already exists`);
return;
}

writeFiles(
[
{
name: `index.ts`,
contents: componentIndex({ packageNamePascal }),
},
{
name: `${packageNamePascal}.tsx`,
TheSonOfThomp marked this conversation as resolved.
Show resolved Hide resolved
contents: component({ packageNamePascal }),
},
{
name: `${packageNamePascal}.spec.tsx`,
contents: spec({ packageNamePascal, packageNameKebab }),
},
{
name: `${packageNamePascal}.types.ts`,
contents: types({ packageNamePascal }),
},
{
name: `${packageNamePascal}.styles.ts`,
contents: styles,
},
],
{
dir: subComponentDir,
packageNamePascal,
},
);
});
}
133 changes: 14 additions & 119 deletions tools/create/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,131 +1,26 @@
/* eslint-disable no-console */
import { getLGConfig } from '@lg-tools/meta';
import chalk from 'chalk';
import fse from 'fs-extra';
import { camelCase, kebabCase, startCase } from 'lodash';
import path from 'path';

import { CreatePackageOptions } from './create.types';
import {
component,
componentIndex,
index,
pkgJson,
readMe,
spec,
story,
styles,
tsConfig,
types,
} from './templates';
import { createComponent } from './createComponent';
import { createSubComponent } from './createSubComponent';

export function createPackage(name: string, options: CreatePackageOptions) {
const rootDir = process.cwd();

const { scopes } = getLGConfig();
const scope = options.scope ?? Object.keys(scopes)[0];
const directory = options.directory ?? scopes[scope];

// Construct all required parameters
const packageNameKebab = kebabCase(name);
const packageNameTitle = startCase(name);
const packageNamePascal = camelCase(name).replace(/^\w/, c =>
c.toUpperCase(),
);

console.log(
chalk.green(
`Creating package ${chalk.bold(scope + '/' + packageNameKebab)}`,
),
);

// Create the appropriate directory
const packageDir = path.resolve(rootDir, directory, packageNameKebab);

// Create the package directories
fse.mkdir(packageDir, { recursive: true }, err => {
if (err) {
console.log(`Package ${packageNameKebab} already exists`);
return;
}
const parent = options.parent;

// Write the appropriate files for each template
writeFiles(
[
{
name: 'package.json',
contents: pkgJson({
scope,
packageNameKebab,
packageNameTitle,
}),
},
{
name: 'tsconfig.json',
contents: tsConfig,
},
{
name: 'README.md',
contents: readMe({ packageNameKebab, packageNameTitle }),
},
{
name: 'src/index.ts',
contents: index({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}.stories.tsx`,
contents: story({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/index.ts`,
contents: componentIndex({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.tsx`,
contents: component({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.spec.tsx`,
contents: spec({ packageNamePascal, packageNameKebab }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.types.ts`,
contents: types({ packageNamePascal }),
},
{
name: `src/${packageNamePascal}/${packageNamePascal}.styles.ts`,
contents: styles,
},
],
{
dir: packageDir,
packageNamePascal,
},
);
});
}

function handleErr(err: NodeJS.ErrnoException | null) {
if (err) throw err;
}

function writeFiles(
files: Array<{
name: string;
contents: string;
}>,
config: {
dir: string;
packageNamePascal: string;
},
) {
// Make the directory src and src/Component
fse.mkdirSync(path.resolve(config.dir, 'src', config.packageNamePascal), {
recursive: true,
});
// TODO: get scope & directory from parent
const scope = options.scope ?? Object.keys(scopes)[0];
const directory = options.directory ?? scopes[scope];

// Write all component files
for (const { name, contents } of files) {
fse.writeFile(path.resolve(config.dir, name), contents, handleErr);
if (parent) {
createSubComponent({ name, parent });
} else {
createComponent({
name,
scope,
directory,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { TemplateParameters } from '../../../../create.types';

export const component = ({
packageNamePascal,
}: Pick<TemplateParameters, 'packageNamePascal'>) => `
import React from 'react';
}: Pick<TemplateParameters, 'packageNamePascal'>) => `import React from 'react';
import { ${packageNamePascal}Props } from './${packageNamePascal}.types';

// TODO: forwardRef
export function ${packageNamePascal}({}) {
export function ${packageNamePascal}({}: ${packageNamePascal}Props) {
return <div>your content here</div>;
}

Expand Down
27 changes: 27 additions & 0 deletions tools/create/src/utils/getComponentPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getLGConfig } from '@lg-tools/meta';
import fse from 'fs-extra';
import path from 'path';

/** Given a component name, return its absolute path */
export function getComponentPath(name: string): string | undefined {
const { scopes } = getLGConfig();
const rootDir = process.cwd();

if (name.startsWith('@')) {
// We have scope defined for us, so we can compute the absolute path
const [scope, packageName] = name.split('/'); // ['@leafygreen-ui', 'button']
const parentDir: string | undefined = scopes[scope]; // 'packages'

if (fse.existsSync(path.resolve(rootDir, parentDir, packageName))) {
return path.resolve(rootDir, parentDir, packageName);
}
} else {
// We need to check all package directories for the component
for (const dir of Object.values(scopes)) {
// ['packages', 'tools'] / 'button'
if (fse.existsSync(path.resolve(rootDir, dir, name))) {
return path.resolve(rootDir, dir, name);
}
}
}
}
Loading
Loading