Skip to content

Commit

Permalink
Merge pull request #66 from jakala-na/feat/component-generate-cli
Browse files Browse the repository at this point in the history
feat(component-generate-cli): implements new component scafollding with CLI
  • Loading branch information
asgorobets authored Nov 7, 2024
2 parents ae2411d + 72c1936 commit 5d43607
Show file tree
Hide file tree
Showing 11 changed files with 624 additions and 30 deletions.
38 changes: 38 additions & 0 deletions cli/generate-component.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import chalk from 'chalk';
import inquirer from 'inquirer';

import prompts from './prompts.mjs';
import { fetchCTFContentTypes, scaffoldComponentFiles } from './utils.mjs';

if (!process.env.CONTENTFUL_SPACE || !process.env.CONTENTFUL_ENVIRONMENT || !process.env.CONTENTFUL_DELIVERY_API) {
console.log(
chalk.whiteBright.bgRed.bold(
"You'll need to provide CONTENTFUL_SPACE, CONTENTFUL_ENVIRONMENT and CONTENTFUL_DELIVERY_API in your .env.local file for the CLI to be functional."
)
);
process.exit();
}

const contentTypesList = await fetchCTFContentTypes(
process.env.CONTENTFUL_SPACE,
process.env.CONTENTFUL_ENVIRONMENT,
process.env.CONTENTFUL_DELIVERY_API,
prompts.promptContentType.choices
);

inquirer
.prompt([
{
...prompts.promptContentType,
choices: contentTypesList,
},
])
.then(({ promptContentType: contentType }) => {
if (contentType === prompts.promptContentType.choices[0]) {
inquirer.prompt([prompts.promptComponentName]).then(({ promptComponentName: componentName }) => {
scaffoldComponentFiles(componentName, false);
});
} else {
scaffoldComponentFiles(contentType, true);
}
});
22 changes: 22 additions & 0 deletions cli/prompts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const prompts = {
promptContentType: {
name: 'promptContentType',
type: 'list',
message: 'Please choose a CTF content type to generate the component for:',
choices: ['---No content type yet---'],
},
promptComponentName: {
name: 'promptComponentName',
type: 'input',
message: 'Please input the component name (use kebab case):',
validate: (input) => {
if (!input || input === '') {
return 'Please provide a component name.';
} else {
return true;
}
},
},
};

export default prompts;
1 change: 1 addition & 0 deletions cli/scaffolds/components/index.{{ext}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { {{pascalCase name}} } from './{{hyphenCase name}}';
17 changes: 17 additions & 0 deletions cli/scaffolds/components/{{hyphenCase name}}-client.{{ext}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { ResultOf } from 'gql.tada';

import { Component{{pascalCase name}}FieldsFragment } from '#/components/{{hyphenCase name}}/{{hyphenCase name}}';
import { {{pascalCase name}} } from '#/components/ui/{{hyphenCase name}}';

import { useComponentPreview } from '../hooks/use-component-preview';

export const {{pascalCase name}}Client: React.FC<{
data: ResultOf<typeof Component{{pascalCase name}}FieldsFragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);

return <{{pascalCase name}} id={data.sys.id} addAttributes={addAttributes} />;
};
21 changes: 21 additions & 0 deletions cli/scaffolds/components/{{hyphenCase name}}.{{ext}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FragmentOf, graphql, readFragment } from 'gql.tada';

import { {{pascalCase name}}Client } from './{{hyphenCase name}}-client';

export const Component{{pascalCase name}}FieldsFragment = graphql(`
fragment Component{{pascalCase name}} on {{pascalCase name}} {
__typename
sys {
id
}
}
`);

export type {{pascalCase name}}Props = {
data: FragmentOf<typeof Component{{pascalCase name}}FieldsFragment>;
};

export const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = (props) => {
const data = readFragment(Component{{pascalCase name}}FieldsFragment, props.data);
return <{{pascalCase name}}Client data={data} />;
};
1 change: 1 addition & 0 deletions cli/scaffolds/ui/index.{{ext}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './{{hyphenCase name}}';
29 changes: 29 additions & 0 deletions cli/scaffolds/ui/{{hyphenCase name}}.stories.{{ext}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from '@storybook/react';

import { {{pascalCase name}} } from './{{hyphenCase name}}';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
title: 'UI/{{startCase name}}',
component: {{pascalCase name}},
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
id: {},
},
} satisfies Meta<typeof {{pascalCase name}}>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Default: Story = {
args: {
id: '{{ snakeCase name }}_id',
},
};
13 changes: 13 additions & 0 deletions cli/scaffolds/ui/{{hyphenCase name}}.{{ext}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface {{pascalCase name}}Props {
id: string;
addAttributes?: (name: string) => object | null;
}

export const {{pascalCase name}} = ({ id, addAttributes = () => ({}) }: {{pascalCase name}}Props) => {
return (
<div {...addAttributes('attributes')}>
<div>{{pascalCase name}}</div>
<div>{id}</div>
</div>
);
};
95 changes: 95 additions & 0 deletions cli/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as fs from 'node:fs';
import path from 'path';
import { fileURLToPath } from 'url';

import chalk from 'chalk';
import * as contentful from 'contentful';
import ora from 'ora';
import { Scaffold } from 'simple-scaffold';

export const pascalToHyphen = (str) => {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
};

function hyphenToPascal(str) {
return str
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}

/*
Fetches all available CTF content types and returns an array with formatted IDs.
*/
export const fetchCTFContentTypes = async (spaceID, envID, token, contentTypesPlaceholder) => {
const ctfClient = contentful.createClient({
space: spaceID,
environment: envID,
accessToken: token,
});
const contentTypes = [...contentTypesPlaceholder];

const spinner = ora(`Fetching content types for space ${spaceID}...`).start();

await ctfClient
.getContentTypes()
.then((response) => {
response.items.forEach((item) => {
contentTypes.push(pascalToHyphen(item.sys.id));
});
spinner.succeed(chalk.green('Done!'));
})
.catch(() => {
console.error();
spinner.fail('Not able to fetch the content types.');
});

return contentTypes;
};

/*
Generates component files using content type or custom input string.
Updates mappings.ts with the new component.
*/
export const scaffoldComponentFiles = (contentType, updateMappings) => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Scaffold component files.
const config = {
name: contentType,
data: {
ext: 'tsx'
},
templates: [path.join(__dirname, 'scaffolds', 'components'), path.join(__dirname, 'scaffolds', 'ui')],
createSubFolder: true,
output: (fullPath, baseDir, baseName) => {
let outputPath = '../components';

if (baseDir.includes('scaffolds/ui')) {
outputPath = '../components/ui';
}

return path.join(__dirname, outputPath, contentType);
},
};

const scaffold = Scaffold(config);

// Update mappings.ts with our new component after a successful scaffold.
if (updateMappings) {
scaffold
.then(() => {
const mappingFilePath = path.join(__dirname, '../components/component-renderer/mappings.ts');
const mappings = fs.readFileSync(mappingFilePath, 'utf-8').split('\n');
mappings.splice(
-2,
0,
` ${hyphenToPascal(contentType)}: dynamic(() => import(\'#/components/${contentType}\').then((mod) => mod.${hyphenToPascal(contentType)})),`
);
const updatedMappings = mappings.join('\n');
fs.writeFileSync(mappingFilePath, updatedMappings, { encoding: 'utf-8' });
})
.catch(() => console.error);
}
};
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"lint:fix": "next lint --fix",
"generate:schema": "source .env.local && gql.tada generate-schema --header \"Authorization: Bearer $CONTENTFUL_DELIVERY_API\" \"https://graphql.contentful.com/content/v1/spaces/$CONTENTFUL_SPACE\"",
"generate:output": "gql.tada generate-output && gql.tada turbo",
"generate:component": "yarn node --env-file=.env.local cli/generate-component.mjs",
"generate:ctf-seed": "contentful space export --export-dir=migrations/ --content-file=ctf-seed.json --use-verbose-renderer=true --download-assets",
"seed": "contentful space import --content-file ./migrations/ctf-seed.json",
"storybook": "storybook dev -p 6006",
Expand Down Expand Up @@ -46,6 +47,7 @@
"gql.tada": "^1.5.1",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.368.0",
"next": "14.2.7",
"next-international": "^1.3.0",
Expand Down Expand Up @@ -77,13 +79,16 @@
"@storybook/nextjs": "^8.0.4",
"@storybook/react": "^8.0.4",
"@types/cypress-image-snapshot": "^3.1.6",
"@types/lodash": "^4.17.13",
"@types/use-analytics": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^8.12.2",
"autoprefixer": "^10.4.14",
"axe-core": "^4.6.1",
"chalk": "^5.3.0",
"chromatic": "^6.21.0",
"commitizen": "^4.3.1",
"contentful": "^11.2.0",
"cypress": "^12.2.0",
"cypress-axe": "^1.2.0",
"cypress-image-snapshot": "^4.0.1",
Expand All @@ -95,12 +100,14 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-tailwindcss": "^3.17.5",
"husky": "^8.0.3",
"inquirer": "9",
"inquirer": "^12.0.1",
"lint-staged": "^14.0.1",
"ora": "^8.1.0",
"postcss": "^8.4.27",
"prettier": "^3.0.3",
"prettier-plugin-curly": "^0.2.1",
"shadcn": "^2.1.0",
"simple-scaffold": "^2.3.2",
"storybook": "^8.0.4",
"tailwindcss": "^3.3.3",
"ts-loader": "^9.4.2",
Expand Down
Loading

0 comments on commit 5d43607

Please sign in to comment.