|
| 1 | +import type * as CEM from 'custom-elements-manifest'; |
| 2 | +import javascript from 'dedent'; |
| 3 | +import { dirname, join, relative } from 'node:path'; |
| 4 | +import { fileURLToPath } from 'node:url'; |
| 5 | +import { readFile, writeFile, mkdir } from 'node:fs/promises'; |
| 6 | + |
| 7 | +function isCustomElementDeclaration(declaration: CEM.Declaration): declaration is CEM.CustomElementDeclaration { |
| 8 | + return !!(declaration as CEM.CustomElementDeclaration).customElement; |
| 9 | +} |
| 10 | + |
| 11 | +function getDeprefixedClassName(className: string) { |
| 12 | + const upper = className.replace('Pf', ''); |
| 13 | + return `${upper.charAt(0).toUpperCase()}${upper.slice(1)}`; |
| 14 | +} |
| 15 | + |
| 16 | +function camel(str: string): string { |
| 17 | + return str.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase()); |
| 18 | +} |
| 19 | + |
| 20 | +function getEventReactPropName(event: CEM.Event) { |
| 21 | + return camel(`on-${event.name}`); |
| 22 | +} |
| 23 | + |
| 24 | +class NonCriticalError extends Error { } |
| 25 | + |
| 26 | +async function writeReactWrapper( |
| 27 | + module: CEM.Module, |
| 28 | + decl: CEM.CustomElementDeclaration, |
| 29 | + outDirPathOrURL: string | URL, |
| 30 | +) { |
| 31 | + const { path, exports } = module; |
| 32 | + if (!exports) { |
| 33 | + throw new Error(`module has no exports: ${path}`); |
| 34 | + } |
| 35 | + const ceExport = exports.find(ex => ex.declaration.name === decl.name); |
| 36 | + if (!ceExport) { |
| 37 | + throw new Error(`module does not export custom element class: ${decl.name}`); |
| 38 | + } |
| 39 | + const { tagName } = decl; |
| 40 | + if (!tagName) { |
| 41 | + throw new NonCriticalError(`declaration does not have a tag name: ${decl.name}`); |
| 42 | + } else { |
| 43 | + const { name: Class } = ceExport; |
| 44 | + const events = decl.events ?? []; |
| 45 | + const outDirPath = |
| 46 | + typeof outDirPathOrURL === 'string' ? outDirPathOrURL |
| 47 | + : fileURLToPath(outDirPathOrURL); |
| 48 | + const outPath = join(outDirPath, path); |
| 49 | + await mkdir(dirname(outPath), { recursive: true }); |
| 50 | + await writeFile(outPath, javascript`// ${path} |
| 51 | + import { createComponent } from '@lit-labs/react'; |
| 52 | + import react from 'react'; |
| 53 | + import { ${Class} as elementClass } from '@patternfly/elements/${module.path}'; |
| 54 | + export const ${getDeprefixedClassName(Class)} = createComponent({ |
| 55 | + tagName: '${decl.tagName}', |
| 56 | + elementClass, |
| 57 | + react, |
| 58 | + events: {${events.map(event => ` |
| 59 | + ${getEventReactPropName(event)}: '${event.name}'`).join(',')}${events.length ? `, |
| 60 | + ` : ''}}, |
| 61 | + }); |
| 62 | +
|
| 63 | + `, 'utf8'); |
| 64 | + return { tagName, outPath }; |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +function isPackage(manifest: unknown): manifest is CEM.Package { |
| 69 | + const maybeManifest = (manifest as CEM.Package); |
| 70 | + return Array.isArray(maybeManifest?.modules) && !!maybeManifest.schemaVersion; |
| 71 | +} |
| 72 | + |
| 73 | +async function parseManifest(maybeManifest: unknown): Promise<CEM.Package> { |
| 74 | + let manifest; |
| 75 | + if (maybeManifest instanceof URL || |
| 76 | + typeof maybeManifest === 'string') { |
| 77 | + manifest = JSON.parse(await readFile(maybeManifest, 'utf-8')); |
| 78 | + } else { |
| 79 | + manifest = maybeManifest; |
| 80 | + } |
| 81 | + if (!isPackage(manifest)) { |
| 82 | + throw new Error('could not parse manifest'); |
| 83 | + } else { |
| 84 | + return manifest; |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +export async function generateReactWrappers( |
| 89 | + customElementsManifestOrPathOrURL: CEM.Package | string | URL, |
| 90 | + outDirPathOrURL: string | URL, |
| 91 | +) { |
| 92 | + const manifest = await parseManifest(customElementsManifestOrPathOrURL); |
| 93 | + const written = []; |
| 94 | + try { |
| 95 | + for (const module of manifest.modules) { |
| 96 | + for (const decl of module.declarations ?? []) { |
| 97 | + if (isCustomElementDeclaration(decl)) { |
| 98 | + written.push(await writeReactWrapper(module, decl, outDirPathOrURL)); |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + } catch (error) { |
| 103 | + if (error instanceof NonCriticalError) { |
| 104 | + // eslint-disable-next-line no-console |
| 105 | + console.info(error.message); |
| 106 | + } else { |
| 107 | + throw error; |
| 108 | + } |
| 109 | + } |
| 110 | + console.group('Wrote React Wrappers'); |
| 111 | + for (const { tagName, outPath } of written) { |
| 112 | + console.log(`${tagName}: ${relative(process.cwd(), outPath)}`); |
| 113 | + } |
| 114 | + console.groupEnd(); |
| 115 | +} |
0 commit comments