-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add create-component script and improve create-package (#16197)
* Update scripts\create-component\create-component.ts * Add create-package and supporting template files Co-authored-by: Josche MacDonnell <[email protected]>
- Loading branch information
1 parent
94b0ffe
commit 1670a00
Showing
19 changed files
with
623 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
// Plop script for templating out a converged React component | ||
//#region Imports | ||
import { NodePlopAPI, AddManyActionConfig } from 'plop'; | ||
import { Actions } from 'node-plop'; | ||
import * as fs from 'fs-extra'; | ||
import * as os from 'os'; | ||
import { spawnSync } from 'child_process'; | ||
import { findGitRoot, getAllPackageInfo } from '../monorepo/index'; | ||
|
||
//#endregion | ||
|
||
//#region Globals | ||
const allPackages = getAllPackageInfo(); | ||
|
||
const root = findGitRoot(); | ||
|
||
const rootPaths = { | ||
package: `packages/{{packageName}}`, | ||
component: `packages/{{packageName}}/src/components/{{componentName}}`, | ||
storybook: `packages/react-examples`, | ||
storybookPackageComponent: `packages/react-examples/src/{{packageName}}/{{componentName}}`, | ||
}; | ||
|
||
const templatePaths = { | ||
component: `plop-templates-component`, | ||
storybookComponent: `plop-templates-storybook`, | ||
// tests: `${root}/create-component/plop-templates-tests`, // Test template implementation for components TBD | ||
}; | ||
|
||
interface Answers { | ||
packageName: string; | ||
packageNpmName: string; | ||
componentName: string; | ||
hasStorybook?: boolean; | ||
doComponentTestBuild?: boolean; | ||
// hasTests?: boolean; // Test template implementation for components TBD | ||
suggest?: any; | ||
} | ||
//#endregion | ||
|
||
//#region Module Export | ||
module.exports = (plop: NodePlopAPI) => { | ||
plop.setWelcomeMessage('This utility is a helper to create converged React components'); | ||
|
||
plop.setActionType('confirmPackageLocation', confirmPackageLocation); | ||
plop.setActionType('appendToPackageIndex', appendToPackageIndex); | ||
plop.setActionType('checkIfComponentAlreadyExists', checkIfComponentAlreadyExists); | ||
|
||
plop.setGenerator('package', { | ||
description: 'New package', | ||
|
||
prompts: [ | ||
{ | ||
type: 'list', | ||
name: 'packageNpmName', | ||
message: 'Which package to create the new component in?', | ||
choices: () => { | ||
return projectsWithStartCommand.map((e, i) => { | ||
return e.title; | ||
}); | ||
}, | ||
}, | ||
|
||
{ | ||
type: 'input', | ||
name: 'componentName', | ||
message: 'New component name (ex: MyComponent):', | ||
when: answers => answers.packageNpmName, | ||
validate: (input: string) => | ||
/^[A-Z][a-zA-Z0-9]+$/.test(input) || 'Must enter a PascalCase component name (ex: MyComponent)', | ||
}, | ||
{ | ||
type: 'confirm', | ||
name: 'doComponentTestBuild', | ||
message: 'Do you wish to run a test build after component creation (Y/n):', | ||
default: true, | ||
when: answers => answers.packageNpmName, | ||
}, | ||
{ | ||
type: 'confirm', | ||
name: 'hasStorybook', | ||
message: 'Will this package have storybook examples? (Y/n):', | ||
default: true, | ||
when: answers => answers.packageNpmName, | ||
}, | ||
// Test template implementation for components TBD | ||
// { | ||
// type: 'confirm', | ||
// name: 'hasTests', | ||
// message: 'Will this component have a test? (Y/n):', | ||
// default: true, | ||
// when: answers => answers.packageNpmName, | ||
// }, | ||
], | ||
|
||
actions: (answers: Answers): Actions => { | ||
const globOptions: AddManyActionConfig['globOptions'] = { dot: true }; | ||
|
||
const data = { | ||
...answers, | ||
...{ | ||
packageName: answers.packageNpmName.replace('@fluentui/', ''), | ||
}, | ||
}; | ||
|
||
const renderString = (text: string): string => { | ||
return plop.renderString(text, data); | ||
}; | ||
|
||
return [ | ||
() => { | ||
return 'Running create-component actions'; | ||
}, | ||
{ | ||
type: 'confirmPackageLocation', | ||
data, | ||
}, | ||
{ | ||
type: 'checkIfComponentAlreadyExists', | ||
data, | ||
}, | ||
{ | ||
// Copy component templates | ||
type: 'addMany', | ||
destination: renderString(rootPaths.package), | ||
globOptions, | ||
data, | ||
skipIfExists: true, | ||
base: 'plop-templates-component', | ||
templateFiles: [`${renderString(templatePaths.component)}/**/*`], | ||
}, | ||
{ | ||
type: 'appendToPackageIndex', | ||
data, | ||
}, | ||
{ | ||
// Copy/Update storybook templates | ||
type: 'addMany', | ||
destination: renderString(rootPaths.storybookPackageComponent), | ||
globOptions, | ||
data, | ||
skipIfExists: true, | ||
skip: () => { | ||
if (!data.hasStorybook) { | ||
return 'Skipping storybook scaffolding'; | ||
} | ||
}, | ||
base: 'plop-templates-storybook', | ||
templateFiles: [`${renderString(templatePaths.storybookComponent)}/**/*`], | ||
}, | ||
() => { | ||
console.log('\nPackage files created! Running yarn to link...\n'); | ||
const yarnResult = spawnSync('yarn', ['--ignore-scripts'], { cwd: root, stdio: 'inherit', shell: true }); | ||
if (yarnResult.status !== 0) { | ||
console.error('Something went wrong with running yarn. Please check previous logs for details'); | ||
process.exit(1); | ||
} | ||
return 'Packages linked!'; | ||
}, | ||
() => { | ||
if (answers.doComponentTestBuild !== true) { | ||
return 'Skipping component test compile.'; | ||
} | ||
|
||
console.log('Component files created! Running yarn build...\n'); | ||
const yarnResult = spawnSync('yarn', [`buildto *${data.packageName}`], { | ||
cwd: root, | ||
stdio: 'inherit', | ||
shell: true, | ||
}); | ||
if (yarnResult.status !== 0) { | ||
console.error('Something went wrong with building. Please check previous logs for details'); | ||
process.exit(1); | ||
} | ||
return 'Component compiled!'; | ||
}, | ||
'\nCreated new component! Please check over it and ensure wording and included files ' + | ||
'make sense for your scenario.', | ||
]; | ||
}, | ||
}); | ||
}; | ||
//#endregion | ||
|
||
//#region Plop Custom Actions | ||
const confirmPackageLocation = (answers: object, config: object, plop: object): string => { | ||
const { packageName } = answers as Answers; | ||
const plopAPI = plop as NodePlopAPI; | ||
const location = plopAPI.renderString(rootPaths.package, answers); | ||
if (fs.existsSync(location) !== true) { | ||
displayAndThrowError( | ||
`**ABORTING** The package ${packageName} cannot be found at location ${location}. Use yarn create-package first.`, | ||
); | ||
} | ||
if (fs.existsSync(`${location}/src/index.ts`) !== true) { | ||
displayAndThrowError(`**ABORTING** The package ${packageName} was found but missing src/index.ts`); | ||
} | ||
|
||
return `Found package ${packageName} at location: ${rootPaths.package}`; | ||
}; | ||
|
||
const checkIfComponentAlreadyExists = (answers: object, config: object, plop: object): string => { | ||
const { componentName } = answers as Answers; | ||
const plopAPI = plop as NodePlopAPI; | ||
const location = plopAPI.renderString(rootPaths.component, answers); | ||
|
||
if (fs.existsSync(location) === true && fs.readdirSync(location).length > 0) { | ||
displayAndThrowError(`**ABORTING** The component ${componentName} already exists at ${location}`); | ||
} | ||
return `Component ${componentName} doesn't exist.`; | ||
}; | ||
|
||
const appendToPackageIndex = async (answers: object, config: object, plop: object): Promise<string> => { | ||
const { componentName, packageName } = answers as Answers; | ||
const plopAPI = plop as NodePlopAPI; | ||
|
||
const options = { flag: 'a' }; | ||
// get the package index file path | ||
const indexPath = plopAPI.renderString(`${rootPaths.package}/src/index.ts`, answers); | ||
const appendLine = plopAPI.renderString(`export * from './{{componentName}}';`, answers); | ||
// read contents and see if line is exists | ||
return fs | ||
.readFile(indexPath, { encoding: 'utf8', flag: 'r' }) | ||
.then(async data => { | ||
const lines = data.split(/\r?\n/); | ||
const getIndex = (arr: string[], item: string): number => | ||
lines.findIndex(line => item.toLocaleLowerCase() === line.toLocaleLowerCase()); | ||
if (getIndex(lines, appendLine) === -1) { | ||
// doesn't exist so append | ||
await fs.outputFile(indexPath, `${appendLine}${os.EOL}`, options); | ||
return `Updated package ${packageName} index.ts to include ${componentName}`; | ||
} | ||
return `Package ${packageName} index.ts already contains reference to ${componentName}`; | ||
}) | ||
.catch(error => { | ||
throw `**ABORTING** There was an error reading index file at ${indexPath}. Error: ${error}`; | ||
}); | ||
}; | ||
//#endregion | ||
|
||
//#region Utilities | ||
const displayAndThrowError = (message: string) => { | ||
console.log(message); | ||
throw message; | ||
}; | ||
|
||
//#endregion | ||
|
||
const ignoreProjects = [ | ||
'@fluentui/azure-themes', | ||
'@fluentui/docs', | ||
'@fluentui/dom-utilities', | ||
'@fluentui/foundation-legacy', | ||
'@fluentui/react-examples', | ||
'@fluentui/react-next', | ||
'@fluentui/react', | ||
'@fluentui/test-utilities', | ||
'@fluentui/theme', | ||
'@fluentui/webpack-utilities', | ||
]; | ||
|
||
const projectsWithStartCommand = Object.entries(allPackages) | ||
.filter( | ||
([pkg, info]) => | ||
!ignoreProjects.includes(pkg) && | ||
info.packagePath.startsWith('packages') && | ||
info.packageJson.dependencies && | ||
info.packageJson.dependencies['@fluentui/react-compose'] !== undefined, | ||
) | ||
.map(([pkg, info]) => ({ title: pkg, value: { pkg, command: 'start' } })); |
10 changes: 10 additions & 0 deletions
10
scripts/create-component/plop-templates-component/src/common/isConformant.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { isConformant as baseIsConformant, IsConformantOptions } from '@fluentui/react-conformance'; | ||
|
||
export function isConformant(testInfo: Omit<IsConformantOptions, 'componentPath'>) { | ||
const defaultOptions = { | ||
disabledTests: ['has-docblock'], | ||
componentPath: module!.parent!.filename.replace('.test', ''), | ||
}; | ||
|
||
baseIsConformant(defaultOptions, testInfo); | ||
} |
6 changes: 6 additions & 0 deletions
6
...s/create-component/plop-templates-component/src/components/{{componentName}}/index.ts.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export * from './{{componentName}}.types'; | ||
export * from './{{componentName}}'; | ||
export * from './render{{componentName}}'; | ||
export * from './use{{componentName}}'; | ||
export * from './use{{componentName}}Classes'; | ||
export * from './use{{componentName}}State'; |
21 changes: 21 additions & 0 deletions
21
...plop-templates-component/src/components/{{componentName}}/render{{componentName}}.tsx.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import * as React from 'react'; | ||
import { getSlots } from '@fluentui/react-compose/lib/next/index'; | ||
import { {{componentName}}State } from './{{componentName}}.types'; | ||
import { {{camelCase componentName}}ShorthandProps } from './use{{componentName}}'; | ||
|
||
/** | ||
* Redefine the render function to add slots. Reuse the {{lowerCase componentName}} structure but add | ||
* slots to children. | ||
*/ | ||
export const render{{componentName}} = (state: {{componentName}}State) => { | ||
const { slots, slotProps } = getSlots(state, {{camelCase componentName}}ShorthandProps); | ||
const { children } = state; | ||
|
||
const contentVisible = (children || slotProps.content?.children); | ||
|
||
return ( | ||
<slots.root {...slotProps.root}> | ||
{contentVisible && <slots.content {...slotProps.content} />} | ||
</slots.root> | ||
); | ||
}; |
35 changes: 35 additions & 0 deletions
35
...ent/plop-templates-component/src/components/{{componentName}}/use{{componentName}}.ts.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as React from 'react'; | ||
import { makeMergeProps, resolveShorthandProps } from '@fluentui/react-compose/lib/next/index'; | ||
import { {{componentName}}Props, {{componentName}}State } from './{{componentName}}.types'; | ||
import { use{{componentName}}State } from './use{{componentName}}State'; | ||
import { useMergedRefs } from '@fluentui/react-hooks'; | ||
|
||
/** | ||
* Consts listing which props are shorthand props. | ||
*/ | ||
export const {{camelCase componentName}}ShorthandProps = ['loader', 'content']; | ||
|
||
const mergeProps = makeMergeProps({ deepMerge: {{camelCase componentName}}ShorthandProps }); | ||
|
||
/** | ||
* Given user props, returns state and render function for a {{componentName}}. | ||
*/ | ||
export const use{{componentName}} = (props: {{componentName}}Props, ref: React.Ref<HTMLElement>, defaultProps?: {{componentName}}Props) => { | ||
// Ensure that the `ref` prop can be used by other things (like useFocusRects) to refer to the root. | ||
// NOTE: We are assuming refs should not mutate to undefined. Either they are passed or not. | ||
const defaultRef = React.useRef<HTMLElement>(null); | ||
const resolvedRef = ref || defaultRef; | ||
|
||
const state = mergeProps( | ||
{ | ||
ref: resolvedRef, | ||
as: 'div' | ||
}, | ||
defaultProps, | ||
resolveShorthandProps(props, {{camelCase componentName}}ShorthandProps), | ||
); | ||
|
||
use{{componentName}}State(state); | ||
|
||
return state as {{componentName}}State; | ||
}; |
16 changes: 16 additions & 0 deletions
16
...p-templates-component/src/components/{{componentName}}/use{{componentName}}Classes.ts.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { makeVariantClasses } from '@fluentui/react-theme-provider'; | ||
import { {{componentName}}State, {{componentName}}Variants } from './{{componentName}}.types'; | ||
|
||
const GlobalClassNames = { | ||
root: 'ms-{{componentName}}' | ||
}; | ||
|
||
export const use{{componentName}}Classes = makeVariantClasses<{{componentName}}State, {{componentName}}Variants>({ | ||
name: '{{componentName}}', | ||
prefix: '--{{lowerCase componentName}}', | ||
styles: { | ||
root: [ | ||
GlobalClassNames.root | ||
] | ||
} | ||
}); |
Oops, something went wrong.