diff --git a/generators/components/package.json b/generators/components/package.json index 4d8c5d0d7..1b8e7c4bb 100644 --- a/generators/components/package.json +++ b/generators/components/package.json @@ -11,7 +11,7 @@ "dist" ], "scripts": { - "build": "yarn library build && cp -R ./src/templates ./dist/templates", + "build": "yarn library build && node ./dist/build-templates.js", "generate-component": "NODE_OPTIONS=\"$NODE_OPTIONS --import tsx\" plop --plopfile=./src/config.ts", "prepack": "yarn run build", "postpack": "rm -rf dist" diff --git a/generators/components/src/build-templates.ts b/generators/components/src/build-templates.ts new file mode 100644 index 000000000..2ee797f84 --- /dev/null +++ b/generators/components/src/build-templates.ts @@ -0,0 +1,87 @@ +import { access } from 'node:fs/promises' +import { cp } from 'node:fs/promises' +import { mkdir } from 'node:fs/promises' +import { readFile } from 'node:fs/promises' +import { readdir } from 'node:fs/promises' +import { rm } from 'node:fs/promises' +import { writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const IMPORT_REPLACEMENTS: Array = [ + ['@atls-ui-admin/theme/theme-css', '@ui/theme/theme-css'], + ['@atls-ui-admin/theme', '@ui/theme'], +] + +const packageRoot = process.cwd() +const repositoryRoot = join(packageRoot, '..', '..') +const examplesPath = join(repositoryRoot, 'ui-admin', 'examples') +const sourceTemplatesPath = join(packageRoot, 'src', 'templates') +const distTemplatesPath = join(packageRoot, 'dist', 'templates') + +const pathExists = async (path: string): Promise => { + try { + await access(path) + + return true + } catch { + return false + } +} + +const replaceImports = (content: string): string => + IMPORT_REPLACEMENTS.reduce( + (result, [source, replacement]) => result.replaceAll(source, replacement), + content + ) + +const copyExampleTemplates = async (sourcePath: string, destinationPath: string): Promise => { + const entries = await readdir(sourcePath, { withFileTypes: true }) + + await mkdir(destinationPath, { recursive: true }) + + await Promise.all( + entries.map(async (entry) => { + const entrySourcePath = join(sourcePath, entry.name) + const entryDestinationPath = join(destinationPath, entry.name) + + if (entry.isDirectory()) { + await copyExampleTemplates(entrySourcePath, entryDestinationPath) + + return + } + + const content = await readFile(entrySourcePath, 'utf8') + + await writeFile(entryDestinationPath, replaceImports(content)) + }) + ) +} + +const readExampleTemplateTypes = async (): Promise> => { + const entries = await readdir(examplesPath, { withFileTypes: true }) + const directories = entries.filter((entry) => entry.isDirectory()) + const existingTypes = await Promise.all( + directories.map(async (entry) => { + const sourcePath = join(examplesPath, entry.name, 'src') + const sourceExists = await pathExists(sourcePath) + + return sourceExists ? entry.name : undefined + }) + ) + + return existingTypes.filter((type): type is string => Boolean(type)) +} + +await rm(distTemplatesPath, { recursive: true, force: true }) +await cp(sourceTemplatesPath, distTemplatesPath, { recursive: true }) + +const exampleTemplateTypes = await readExampleTemplateTypes() + +await Promise.all( + exampleTemplateTypes.map(async (type) => { + await copyExampleTemplates( + join(repositoryRoot, 'ui-admin', 'examples', type, 'src'), + join(distTemplatesPath, type) + ) + }) +) diff --git a/generators/components/src/config.ts b/generators/components/src/config.ts index 72d39a96c..b73db500c 100644 --- a/generators/components/src/config.ts +++ b/generators/components/src/config.ts @@ -1,13 +1,103 @@ +/* eslint-disable n/no-sync */ + import type { NodePlopAPI } from 'plop' +import { existsSync } from 'node:fs' +import { readdirSync } from 'node:fs' import { copyFile } from 'node:fs/promises' import { mkdir } from 'node:fs/promises' +import { readFile } from 'node:fs/promises' import { readdir } from 'node:fs/promises' +import { writeFile } from 'node:fs/promises' +import { dirname } from 'node:path' import { join } from 'node:path' +import { resolve } from 'node:path' const RAW_TEMPLATE_EXTENSION = '.raw' -const copyRawTemplates = async ( +const IMPORT_REPLACEMENTS: Array = [ + ['@atls-ui-admin/theme/theme-css', '@ui/theme/theme-css'], + ['@atls-ui-admin/theme', '@ui/theme'], +] + +const findRepositoryRoot = (startPath: string): string | undefined => { + const currentPath = resolve(startPath) + + if (existsSync(join(currentPath, 'ui-admin', 'examples'))) { + return currentPath + } + + const parentPath = dirname(currentPath) + + if (parentPath === currentPath) { + return undefined + } + + return findRepositoryRoot(parentPath) +} + +const readDirectoryNames = (path: string): Array => { + if (!existsSync(path)) { + return [] + } + + const entries = readdirSync(path, { withFileTypes: true }) + + return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name) +} + +const resolveRepositoryExamplePath = (plopfilePath: string, type: string): string | undefined => { + const repositoryRoot = findRepositoryRoot(plopfilePath) + const exampleSourcePath = repositoryRoot + ? join(repositoryRoot, 'ui-admin', 'examples', type, 'src') + : '' + + if (exampleSourcePath && existsSync(exampleSourcePath)) { + return exampleSourcePath + } + + return undefined +} + +const getRepositoryExampleTypes = (plopfilePath: string): Array => { + const repositoryRoot = findRepositoryRoot(plopfilePath) + + if (!repositoryRoot) { + return [] + } + + const examplesPath = join(repositoryRoot, 'ui-admin', 'examples') + const exampleTypes = readDirectoryNames(examplesPath) + + return exampleTypes.filter((type) => existsSync(join(examplesPath, type, 'src'))) +} + +const getTemplateTypes = (plopfilePath: string): Array => { + const packagedTypes = readDirectoryNames(join(plopfilePath, 'templates')) + const exampleTypes = getRepositoryExampleTypes(plopfilePath) + + return [...new Set([...packagedTypes, ...exampleTypes])].sort() +} + +const getTemplateName = (type: string): string => + type + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' ') + +const getTemplateChoices = (plopfilePath: string): Array<{ name: string; value: string }> => { + const templateTypes = getTemplateTypes(plopfilePath) + + return templateTypes.map((type) => ({ name: getTemplateName(type), value: type })) +} + +const replaceImports = (content: string): string => + IMPORT_REPLACEMENTS.reduce( + (result, [source, replacement]) => result.replaceAll(source, replacement), + content + ) + +const copyExampleTemplates = async ( sourcePath: string, destinationPath: string ): Promise => { @@ -16,21 +106,80 @@ const copyRawTemplates = async ( await mkdir(destinationPath, { recursive: true }) - const copiedCounts = await Promise.all(entries.map(async (entry) => { + const copiedCounts = await Promise.all( + entries.map(async (entry) => { + const entrySourcePath = join(sourcePath, entry.name) + const entryDestinationPath = join(destinationPath, entry.name) + + if (entry.isDirectory()) { + return copyExampleTemplates(entrySourcePath, entryDestinationPath) + } + + const content = await readFile(entrySourcePath, 'utf8') + + await writeFile(entryDestinationPath, replaceImports(content)) + + return 1 + }) + ) + + return copiedCounts.reduce((total, count) => total + count, copied) +} + +const hasTemplateExtension = (sourcePath: string): boolean => { + const entries = readdirSync(sourcePath, { withFileTypes: true }) + const checks = entries.map((entry) => { const entrySourcePath = join(sourcePath, entry.name) - const entryName = entry.name.endsWith(RAW_TEMPLATE_EXTENSION) - ? entry.name.slice(0, -RAW_TEMPLATE_EXTENSION.length) - : entry.name - const entryDestinationPath = join(destinationPath, entryName) if (entry.isDirectory()) { - return copyRawTemplates(entrySourcePath, entryDestinationPath) + return hasTemplateExtension(entrySourcePath) } - await copyFile(entrySourcePath, entryDestinationPath) + return entry.name.endsWith('.hbs') || entry.name.endsWith('.raw') + }) + + return checks.some(Boolean) +} + +const resolveCopyTemplatePath = (plopfilePath: string, type: string): string | undefined => { + const exampleSourcePath = resolveRepositoryExamplePath(plopfilePath, type) + + if (exampleSourcePath) { + return exampleSourcePath + } + + const packagedTemplatePath = join(plopfilePath, 'templates', type) + + if (existsSync(packagedTemplatePath) && !hasTemplateExtension(packagedTemplatePath)) { + return packagedTemplatePath + } + + return undefined +} + +const copyRawTemplates = async (sourcePath: string, destinationPath: string): Promise => { + const entries = await readdir(sourcePath, { withFileTypes: true }) + const copied = 0 + + await mkdir(destinationPath, { recursive: true }) + + const copiedCounts = await Promise.all( + entries.map(async (entry) => { + const entrySourcePath = join(sourcePath, entry.name) + const entryName = entry.name.endsWith(RAW_TEMPLATE_EXTENSION) + ? entry.name.slice(0, -RAW_TEMPLATE_EXTENSION.length) + : entry.name + const entryDestinationPath = join(destinationPath, entryName) + + if (entry.isDirectory()) { + return copyRawTemplates(entrySourcePath, entryDestinationPath) + } + + await copyFile(entrySourcePath, entryDestinationPath) - return 1 - })) + return 1 + }) + ) return copiedCounts.reduce((total, count) => total + count, copied) } @@ -43,18 +192,22 @@ const generator = (plop: NodePlopAPI): void => { type: 'list', name: 'type', message: 'Select component type:', - choices: [ - { name: 'Bottom Navigation', value: 'bottom-navigation' }, - { name: 'Checkbox', value: 'checkbox' }, - { name: 'Modal', value: 'modal' }, - { name: 'Popover', value: 'popover' }, - { name: 'Sidebar', value: 'sidebar' }, - { name: 'Tooltip', value: 'tooltip' }, - ], + choices: getTemplateChoices(plop.getPlopfilePath()), }, ], actions: (data) => { const type = typeof data?.type === 'string' ? data.type : '' + const copyTemplatePath = resolveCopyTemplatePath(plop.getPlopfilePath(), type) + + if (copyTemplatePath) { + return [ + async (): Promise => { + const copied = await copyExampleTemplates(copyTemplatePath, join(process.cwd(), 'src')) + + return `${copied} files copied` + }, + ] + } if (type === 'checkbox') { return [ diff --git a/generators/components/src/templates/bottom-navigation/bottom-navigation-item/component.tsx.hbs b/generators/components/src/templates/bottom-navigation/bottom-navigation-item/component.tsx.hbs deleted file mode 100644 index 2f69d2cdb..000000000 --- a/generators/components/src/templates/bottom-navigation/bottom-navigation-item/component.tsx.hbs +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentType } from 'react' -import type { ReactElement } from 'react' - -import { createElement } from 'react' - -import { vars } from '@ui/theme' - -import { bottomNavigationItemStyles } from './styles.css.js' - -export interface BottomNavigationItemProps { - icon: ComponentType<{ - color?: string - width?: number | string - height?: number | string - }> -} - -export const BottomNavigationItem = ({ icon }: BottomNavigationItemProps): ReactElement => ( -
- {createElement(icon, { - color: vars.colors.white, - width: 20, - height: 20, - })} -
-) diff --git a/generators/components/src/templates/bottom-navigation/bottom-navigation-item/index.ts.hbs b/generators/components/src/templates/bottom-navigation/bottom-navigation-item/index.ts.hbs deleted file mode 100644 index b7a151031..000000000 --- a/generators/components/src/templates/bottom-navigation/bottom-navigation-item/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export * from './component.js' diff --git a/generators/components/src/templates/bottom-navigation/bottom-navigation-item/styles.css.ts.hbs b/generators/components/src/templates/bottom-navigation/bottom-navigation-item/styles.css.ts.hbs deleted file mode 100644 index 1d5ef0fec..000000000 --- a/generators/components/src/templates/bottom-navigation/bottom-navigation-item/styles.css.ts.hbs +++ /dev/null @@ -1,9 +0,0 @@ -import { style } from '@vanilla-extract/css' - -export const bottomNavigationItemStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - boxSizing: 'border-box', - width: '100%', -}) diff --git a/generators/components/src/templates/bottom-navigation/bottom-navigation-root/component.tsx.hbs b/generators/components/src/templates/bottom-navigation/bottom-navigation-root/component.tsx.hbs deleted file mode 100644 index 65a2ca8f9..000000000 --- a/generators/components/src/templates/bottom-navigation/bottom-navigation-root/component.tsx.hbs +++ /dev/null @@ -1,8 +0,0 @@ -import type { PropsWithChildren } from 'react' -import type { ReactElement } from 'react' - -import { bottomNavigationRootStyles } from './styles.css.js' - -export const BottomNavigationRoot = ({ children }: PropsWithChildren): ReactElement => ( -
{children}
-) diff --git a/generators/components/src/templates/bottom-navigation/bottom-navigation-root/index.ts.hbs b/generators/components/src/templates/bottom-navigation/bottom-navigation-root/index.ts.hbs deleted file mode 100644 index b7a151031..000000000 --- a/generators/components/src/templates/bottom-navigation/bottom-navigation-root/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export * from './component.js' diff --git a/generators/components/src/templates/bottom-navigation/bottom-navigation-root/styles.css.ts.hbs b/generators/components/src/templates/bottom-navigation/bottom-navigation-root/styles.css.ts.hbs deleted file mode 100644 index 1eb17dafc..000000000 --- a/generators/components/src/templates/bottom-navigation/bottom-navigation-root/styles.css.ts.hbs +++ /dev/null @@ -1,13 +0,0 @@ -import { style } from '@vanilla-extract/css' - -import { vars } from '@ui/theme/theme-css' - -export const bottomNavigationRootStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - boxSizing: 'border-box', - background: vars.colors.black, - width: '100%', - height: 64, -}) diff --git a/generators/components/src/templates/bottom-navigation/component.tsx.hbs b/generators/components/src/templates/bottom-navigation/component.tsx.hbs deleted file mode 100644 index adc39eee3..000000000 --- a/generators/components/src/templates/bottom-navigation/component.tsx.hbs +++ /dev/null @@ -1,18 +0,0 @@ -import type { ReactElement } from 'react' - -import type { BottomNavigationItemProps } from './bottom-navigation-item/index.js' - -import { BottomNavigationItem } from './bottom-navigation-item/index.js' -import { BottomNavigationRoot } from './bottom-navigation-root/index.js' - -export interface BottomNavigationProps { - items: Array -} - -export const BottomNavigation = ({ items = [] }: BottomNavigationProps): ReactElement => ( - - {items.map((item) => ( - - ))} - -) diff --git a/generators/components/src/templates/bottom-navigation/index.ts.hbs b/generators/components/src/templates/bottom-navigation/index.ts.hbs deleted file mode 100644 index b7a151031..000000000 --- a/generators/components/src/templates/bottom-navigation/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export * from './component.js' diff --git a/generators/components/src/templates/sidebar/component.tsx.hbs b/generators/components/src/templates/sidebar/component.tsx.hbs deleted file mode 100644 index 6043dbd6f..000000000 --- a/generators/components/src/templates/sidebar/component.tsx.hbs +++ /dev/null @@ -1,22 +0,0 @@ -import type { ReactElement } from 'react' -import type { ReactNode } from 'react' - -import type { SidebarItemProps } from './sidebar-item/index.js' - -import { SidebarItem } from './sidebar-item/index.js' -import { SidebarLogo } from './sidebar-logo/index.js' -import { SidebarRoot } from './sidebar-root/index.js' - -export interface SidebarProps { - items: Array - logo?: ReactNode -} - -export const Sidebar = ({ logo, items = [] }: SidebarProps): ReactElement => ( - - {!!logo && {logo}} - {items.map((item) => ( - - ))} - -) diff --git a/generators/components/src/templates/sidebar/index.ts.hbs b/generators/components/src/templates/sidebar/index.ts.hbs deleted file mode 100644 index b7a151031..000000000 --- a/generators/components/src/templates/sidebar/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export * from './component.js' diff --git a/generators/components/src/templates/sidebar/sidebar-item/component.tsx.hbs b/generators/components/src/templates/sidebar/sidebar-item/component.tsx.hbs deleted file mode 100644 index 3b9d07370..000000000 --- a/generators/components/src/templates/sidebar/sidebar-item/component.tsx.hbs +++ /dev/null @@ -1,24 +0,0 @@ -import type { ReactElement } from 'react' -import type { ComponentType } from 'react' - -import { createElement } from 'react' - -import { sidebarItemStyles } from './styles.css.js' - -export interface SidebarItemProps { - icon: ComponentType<{ - color?: string - width?: number | string - height?: number | string - }> -} - -export const SidebarItem = ({ icon }: SidebarItemProps): ReactElement => ( -
- {createElement(icon, { - color: 'white', - width: 20, - height: 20, - })} -
-) diff --git a/generators/components/src/templates/sidebar/sidebar-item/index.ts.hbs b/generators/components/src/templates/sidebar/sidebar-item/index.ts.hbs deleted file mode 100644 index b7a151031..000000000 --- a/generators/components/src/templates/sidebar/sidebar-item/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export * from './component.js' diff --git a/generators/components/src/templates/sidebar/sidebar-item/styles.css.ts.hbs b/generators/components/src/templates/sidebar/sidebar-item/styles.css.ts.hbs deleted file mode 100644 index abcccfb98..000000000 --- a/generators/components/src/templates/sidebar/sidebar-item/styles.css.ts.hbs +++ /dev/null @@ -1,10 +0,0 @@ -import { style } from '@vanilla-extract/css' - -export const sidebarItemStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - boxSizing: 'border-box', - width: '100%', - margin: 16, -}) diff --git a/generators/components/src/templates/sidebar/sidebar-logo/component.tsx.hbs b/generators/components/src/templates/sidebar/sidebar-logo/component.tsx.hbs deleted file mode 100644 index 227081e4d..000000000 --- a/generators/components/src/templates/sidebar/sidebar-logo/component.tsx.hbs +++ /dev/null @@ -1,12 +0,0 @@ -import type { ReactElement } from 'react' -import type { ReactNode } from 'react' - -import { sidebarLogoStyles } from './styles.css.js' - -export interface SidebarLogoProps { - children: ReactNode -} - -export const SidebarLogo = ({ children }: SidebarLogoProps): ReactElement => ( -
{children}
-) diff --git a/generators/components/src/templates/sidebar/sidebar-logo/index.ts.hbs b/generators/components/src/templates/sidebar/sidebar-logo/index.ts.hbs deleted file mode 100644 index b7a151031..000000000 --- a/generators/components/src/templates/sidebar/sidebar-logo/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export * from './component.js' diff --git a/generators/components/src/templates/sidebar/sidebar-logo/styles.css.ts.hbs b/generators/components/src/templates/sidebar/sidebar-logo/styles.css.ts.hbs deleted file mode 100644 index 7daba312e..000000000 --- a/generators/components/src/templates/sidebar/sidebar-logo/styles.css.ts.hbs +++ /dev/null @@ -1,10 +0,0 @@ -import { style } from '@vanilla-extract/css' - -export const sidebarLogoStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - boxSizing: 'border-box', - width: 40, - margin: 16, -}) diff --git a/generators/components/src/templates/sidebar/sidebar-root/component.tsx.hbs b/generators/components/src/templates/sidebar/sidebar-root/component.tsx.hbs deleted file mode 100644 index 656afe1f2..000000000 --- a/generators/components/src/templates/sidebar/sidebar-root/component.tsx.hbs +++ /dev/null @@ -1,12 +0,0 @@ -import type { ReactElement } from 'react' -import type { ReactNode } from 'react' - -import { sidebarRootStyles } from './styles.css.js' - -export interface SidebarRootProps { - children?: ReactNode -} - -export const SidebarRoot = ({ children }: SidebarRootProps): ReactElement => ( -
{children}
-) diff --git a/generators/components/src/templates/sidebar/sidebar-root/index.ts.hbs b/generators/components/src/templates/sidebar/sidebar-root/index.ts.hbs deleted file mode 100644 index b7a151031..000000000 --- a/generators/components/src/templates/sidebar/sidebar-root/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export * from './component.js' diff --git a/generators/components/src/templates/sidebar/sidebar-root/styles.css.ts.hbs b/generators/components/src/templates/sidebar/sidebar-root/styles.css.ts.hbs deleted file mode 100644 index e3e06649e..000000000 --- a/generators/components/src/templates/sidebar/sidebar-root/styles.css.ts.hbs +++ /dev/null @@ -1,16 +0,0 @@ -import { style } from '@vanilla-extract/css' - -import { vars } from '@ui/theme/theme-css' - -export const sidebarRootStyles = style({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - boxSizing: 'border-box', - background: vars.colors.blue, - height: '100%', - width: 64, - borderRightColor: vars.colors.gray, - borderRightWidth: 1, - borderRightStyle: 'solid', -})