Skip to content

Commit

Permalink
VACMS-10355 component generator (#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjheffner authored Dec 28, 2023
1 parent 2566a4b commit d60d3c1
Show file tree
Hide file tree
Showing 209 changed files with 1,238 additions and 219 deletions.
18 changes: 13 additions & 5 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
# dependencies & GHA
.github
.next
.yarn
.swc
coverage
node_modules
public
yarn.lock
storybook-static
out
.yarnrc.yml

# test output
coverage

# static outputs
public
.next
out
typedocs
storybook-static
packages/*/dist

# prettier doesn't play well with .hbs format
generator-templates/
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
24 changes: 24 additions & 0 deletions READMEs/generators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Next-build generator usage

Developers contributing to this project can use [plop](https://plopjs.com/) to generate new files in a consistent manner.

There can be many moving parts that need to be aligned when adding a new data query or content type to next-build. Running `yarn plop` will start a cli tool that handles most of boilerplate, freeing you up to focus on the task at hand.

Run `yarn plop` to view a list of generators available to this project.

See `plopfile.js` for specifics of each generator.

Templates for existing generators can be found inside of `generator-templates`.

## What is Plop?

> Plop is what I like to call a "micro-generator framework." Now, I call it that because it is a small tool that gives you a simple way to generate code or any other type of flat text files in a consistent way. You see, we all create structures and patterns in our code (routes, controllers, components, helpers, etc). These patterns change and improve over time so when you need to create a NEW insert-name-of-pattern-here, it's not always easy to locate the files in your codebase that represent the current "best practice." That's where plop saves you. With plop, you have your "best practice" method of creating any given pattern in CODE. Code that can easily be run from the terminal by typing plop. Not only does this save you from hunting around in your codebase for the right files to copy, but it also turns "the right way" into "the easiest way" to make new files.
> If you boil plop down to its core, it is basically glue code between inquirer prompts and handlebar templates.
Helpful documentation:

- [Plop docs](https://plopjs.com/documentation/)
- [Inquirer options](https://github.com/SBoudrias/Inquirer.js/blob/master/packages/inquirer/README.md#question)
- [Handlebars](https://github.com/handlebars-lang/handlebars.js?tab=readme-ov-file#usage)
- [Example plopfile](https://github.com/plopjs/plop/blob/main/packages/plop/tests/examples/javascript/plopfile.js)
11 changes: 11 additions & 0 deletions generator-templates/component/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type {{pascalCase name}}Props = {
title: string
}

export function {{pascalCase name}}({ title }: {{pascalCase name}}Props) {
return (
<div>
<p>{title}</p>
</div>
)
}
21 changes: 21 additions & 0 deletions generator-templates/component/playwright.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { test, expect } = require('../utils/next-test')

test.describe('{{titleCase name}}', () => {
test('{{titleCase name}} page renders', async ({
page,
}) => {
await page.goto('/update-this-link')
await expect(page).toHaveURL('/update-this-link')
})

test('Should render without a11y errors', async ({
page,
makeAxeBuilder,
}) => {
await page.goto('/update-this-link')

const accessibilityScanResults = await makeAxeBuilder().analyze()

expect(accessibilityScanResults.violations).toEqual([])
})
})
17 changes: 17 additions & 0 deletions generator-templates/component/story.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Meta, StoryObj } from '@storybook/react'

import { {{pascalCase name}} } from './index'

const meta: Meta<typeof {{pascalCase name}}> = {
title: 'Uncategorized/{{pascalCase name}}',
component: {{pascalCase name}},
}
export default meta

type Story = StoryObj<typeof {{pascalCase name}}>

export const Example: Story = {
args: {
title: 'Hello World!'
},
}
11 changes: 11 additions & 0 deletions generator-templates/component/test.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { render, screen } from '@testing-library/react'
import { {{pascalCase name}} } from './index'


describe('{{pascalCase name}} with valid data', () => {
test('renders {{pascalCase name}} component', () => {
render(<{{pascalCase name}} title={'Hello world'} />)

expect(screen.queryByText(/Hello world/)).toBeInTheDocument()
})
})
4 changes: 4 additions & 0 deletions generator-templates/query/mock.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"note": "Replace this data! The file should contain an example response for the desired query before formatting",
"example": "See src/mocks/newsStory.mock.json for an example response from va.gov-cms's JSON:API"
}
65 changes: 65 additions & 0 deletions generator-templates/query/query.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { QueryData, QueryFormatter, QueryParams } from 'next-drupal-query'
import { drupalClient } from '@/lib/drupal/drupalClient'
import { queries } from '.'
import { Node{{pascalCase name}} } from '@/types/drupal/node'
import { {{pascalCase name}} } from '@/types/formatted/{{pascalCase name}}'
import { ExpandedStaticPropsContext } from '@/lib/drupal/staticProps'

// Define the query params for fetching node--{{snakeCase name}}.
export const params: QueryParams<null> = () => {
return queries
.getParams()
// uncomment to add referenced entity data to the response
// .addInclude([
// 'field_media',
// 'field_media.image',
// 'field_administration',
// ])
}

// Define the option types for the data loader.
export type {{pascalCase name}}DataOpts = {
id: string
context?: ExpandedStaticPropsContext
}

// Implement the data loader.
export const data: QueryData<{{pascalCase name}}DataOpts, Node{{pascalCase name}}> = async (
opts
): Promise<Node{{pascalCase name}}> => {
const entity = opts?.context?.preview
? // need to use getResourceFromContext for unpublished revisions
await drupalClient.getResourceFromContext<Node{{pascalCase name}}>(
'node--{{snakeCase name}}',
opts.context,
{
params: params().getQueryObject(),
}
)
: // otherwise just lookup by uuid
await drupalClient.getResource<Node{{pascalCase name}}>(
'node--{{snakeCase name}}',
opts.id,
{
params: params().getQueryObject(),
}
)

return entity
}

export const formatter: QueryFormatter<Node{{pascalCase name}}, {{pascalCase name}}> = (
entity: Node{{pascalCase name}}
) => {
return {
id: entity.id,
entityId: entity.drupal_internal__nid,
entityPath: entity.path.alias,
type: entity.type,
published: entity.status,
moderationState: entity.moderation_state,
title: entity.title,
metatags: entity.metatag,
breadcrumbs: entity.breadcrumbs
}
}
25 changes: 25 additions & 0 deletions generator-templates/query/test.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { {{pascalCase name}} } from '@/types/drupal/node'
import { queries } from '@/data/queries'
import mockData from '@/mocks/{{pascalCase name}}.mock.json'

const {{pascalCase name}}Mock: {{pascalCase name}} = mockData

describe('{{pascalCase name}} formatData', () => {
let windowSpy

beforeEach(() => {
windowSpy = jest.spyOn(window, 'window', 'get')
})

afterEach(() => {
windowSpy.mockRestore()
})

test('outputs formatted data', () => {
windowSpy.mockImplementation(() => undefined)

expect(
queries.formatData('node--{{snakeCase name}}', {{pascalCase name}}Mock)
).toMatchSnapshot()
})
})
3 changes: 3 additions & 0 deletions generator-templates/type/formatted.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type {{pascalCase name}} = {
title: string
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"redis:stop": "docker stop next-redis && docker rm next-redis",
"format": "prettier --write .",
"lint": "next lint",
"plop": "plop",
"postinstall": "husky install",
"typedoc": "typedoc",
"typedoc:serve": "http-server typedocs -p 8002",
Expand Down Expand Up @@ -66,6 +67,7 @@
"next-drupal": "^1.6.0",
"next-drupal-query": "^0.4.0",
"next-sitemap": "^4.2.3",
"plop": "^4.0.0",
"proxy-fetcher": "0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
131 changes: 131 additions & 0 deletions plopfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Run `yarn plop` to use the generators defined in this file.
// see READMEs/generators.md for more information.
module.exports = function (plop) {
// Create a new component with a test stub and Storybook entry.
plop.setGenerator('Component', {
description: 'New React component',
prompts: [
{
type: 'input',
name: 'name',
message: 'Component Name',
},
],
actions: [
{
type: 'add',
path: 'src/templates/components/{{camelCase name}}/index.tsx',
templateFile: 'generator-templates/component/index.hbs',
},
{
type: 'add',
path: 'src/templates/components/{{camelCase name}}/index.test.tsx',
templateFile: 'generator-templates/component/test.hbs',
},
{
type: 'add',
path: 'src/templates/components/{{camelCase name}}/{{camelCase name}}.stories.ts',
templateFile: 'generator-templates/component/story.hbs',
},
],
})

// Create a new data query. This defaults to Drupal boilerplate.
// TODO: option for non-drupal data sources.
plop.setGenerator('Query', {
description: 'New Data query',
prompts: [
{
type: 'input',
name: 'name',
message: 'Query name please',
},
],
actions: [
{
type: 'add',
path: 'src/data/queries/{{camelCase name}}.ts',
templateFile: 'generator-templates/query/query.hbs',
},
{
type: 'add',
path: 'src/data/queries/tests/{{camelCase name}}.test.tsx',
templateFile: 'generator-templates/query/test.hbs',
},
{
type: 'add',
path: 'src/mocks/{{camelCase name}}.mock.json',
templateFile: 'generator-templates/query/mock.hbs',
},
{
type: 'add',
path: 'src/types/formatted/{{camelCase name}}.ts',
templateFile: 'generator-templates/type/formatted.hbs',
},
// Strings can be added to print a comment in the terminal.
'You will need to manually import & add your query to src/data/queries/index.ts',
'Be sure to also run `yarn test:u` to update test snapshots for your new query!',
],
})

// Generate all files needed to render a new content type from Drupal.
// It also generates an additional test file for E2E testing the page via Playwright.
plop.setGenerator('Content Type', {
description: 'Generate boilerplate for new FE Page based on Content Type',
prompts: [
{
type: 'input',
name: 'name',
message: 'Page name please',
},
],
actions: [
// Create query files for new Page type.
{
type: 'add',
path: 'src/data/queries/{{camelCase name}}.ts',
templateFile: 'generator-templates/query/query.hbs',
},
{
type: 'add',
path: 'src/data/queries/tests/{{camelCase name}}.test.tsx',
templateFile: 'generator-templates/query/test.hbs',
},
{
type: 'add',
path: 'src/mocks/{{camelCase name}}.mock.json',
templateFile: 'generator-templates/query/mock.hbs',
},
{
type: 'add',
path: 'src/types/formatted/{{camelCase name}}.ts',
templateFile: 'generator-templates/type/formatted.hbs',
},
'You will need to manually import & add your query to src/data/queries/index.ts',
'Be sure to also run `yarn test:u` to update test snapshots for your new query!',
// Create react component + test files for new Page type.
{
type: 'add',
path: 'src/templates/layouts/{{camelCase name}}/index.tsx',
templateFile: 'generator-templates/component/index.hbs',
},
{
type: 'add',
path: 'src/templates/layouts/{{camelCase name}}/index.test.tsx',
templateFile: 'generator-templates/component/test.hbs',
},
{
type: 'add',
path: 'src/templates/layouts/{{camelCase name}}/{{camelCase name}}.stories.ts',
templateFile: 'generator-templates/component/story.hbs',
},
{
type: 'add',
path: 'playwright/tests/{{camelCase name}}.spec.js',
templateFile: 'generator-templates/component/playwright.hbs',
},
],
})

// Add additional generators here
}
4 changes: 2 additions & 2 deletions src/data/queries/alert.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Define the query params for fetching block--alert.
import { BlockAlert } from '@/types/dataTypes/drupal/block'
import { BlockAlert } from '@/types/drupal/block'
import { QueryFormatter } from 'next-drupal-query'
import { Alert } from '@/types/dataTypes/formatted/alert'
import { Alert } from '@/types/formatted/alert'

export const formatter: QueryFormatter<BlockAlert, Alert> = (
entity: BlockAlert
Expand Down
4 changes: 2 additions & 2 deletions src/data/queries/audienceTopics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Define the query params for fetching node--news_story.
import { ParagraphAudienceTopics } from '@/types/dataTypes/drupal/paragraph'
import { ParagraphAudienceTopics } from '@/types/drupal/paragraph'
import { QueryFormatter } from 'next-drupal-query'
import { AudienceTopic } from '@/types/dataTypes/formatted/audienceTopics'
import { AudienceTopic } from '@/types/formatted/audienceTopics'

const getTagsList = (fieldTags) => {
if (!fieldTags) return null
Expand Down
8 changes: 2 additions & 6 deletions src/data/queries/banners.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { QueryFormatter } from 'next-drupal-query'
import { NodeBanner } from '@/types/dataTypes/drupal/node'
import {
Banner,
FacilityBanner,
PromoBanner,
} from '@/types/dataTypes/formatted/banners'
import { NodeBanner } from '@/types/drupal/node'
import { Banner, FacilityBanner, PromoBanner } from '@/types/formatted/banners'

export const BannerDisplayType = {
PROMO_BANNER: 'promoBanner',
Expand Down
Loading

0 comments on commit d60d3c1

Please sign in to comment.