[FRONT COMPONENTS] Move to twenty-sdk (#17587)
Move front components from twenty-shared to twenty-sdk
This commit is contained in:
@@ -1,159 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
import * as prettier from '@prettier/sync';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { IndentationText, Project, QuoteKind } from 'ts-morph';
|
||||
|
||||
import { ALLOWED_HTML_ELEMENTS } from '../../src/front-component-constants/AllowedHtmlElements';
|
||||
import { COMMON_HTML_EVENTS } from '../../src/front-component-constants/CommonHtmlEvents';
|
||||
import { EVENT_TO_REACT } from '../../src/front-component-constants/EventToReact';
|
||||
import { HTML_COMMON_PROPERTIES } from '../../src/front-component-constants/HtmlCommonProperties';
|
||||
|
||||
import {
|
||||
type ComponentSchema,
|
||||
extractHtmlTag,
|
||||
generateHostRegistry,
|
||||
generateRemoteComponents,
|
||||
generateRemoteElements,
|
||||
HtmlElementConfigArrayZ,
|
||||
OUTPUT_FILES,
|
||||
} from './generators';
|
||||
|
||||
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
||||
const PACKAGE_PATH = path.resolve(SCRIPT_DIR, '../..');
|
||||
const FRONT_COMPONENT_PATH = path.join(PACKAGE_PATH, 'src/front-component');
|
||||
const HOST_GENERATED_DIR = path.join(FRONT_COMPONENT_PATH, 'host/generated');
|
||||
const REMOTE_GENERATED_DIR = path.join(
|
||||
FRONT_COMPONENT_PATH,
|
||||
'remote/generated',
|
||||
);
|
||||
|
||||
const formatZodError = (error: {
|
||||
issues: { path: PropertyKey[]; message: string }[];
|
||||
}): string => {
|
||||
return error.issues
|
||||
.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const getHtmlElementSchemas = (): ComponentSchema[] => {
|
||||
const result = HtmlElementConfigArrayZ.safeParse(ALLOWED_HTML_ELEMENTS);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Invalid HTML element configuration:\n${formatZodError(result.error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return result.data.map((element) => ({
|
||||
name: element.name,
|
||||
tagName: element.name,
|
||||
customElementName: element.tag,
|
||||
properties: {
|
||||
...HTML_COMMON_PROPERTIES,
|
||||
...element.properties,
|
||||
},
|
||||
events: COMMON_HTML_EVENTS,
|
||||
isHtmlElement: true,
|
||||
htmlTag: extractHtmlTag(element.tag),
|
||||
}));
|
||||
};
|
||||
|
||||
const createProject = (): Project => {
|
||||
return new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
quoteKind: QuoteKind.Single,
|
||||
useTrailingCommas: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const writeGeneratedFile = (
|
||||
dir: string,
|
||||
filename: string,
|
||||
content: string,
|
||||
): void => {
|
||||
const filePath = path.join(dir, filename);
|
||||
const formattedContent = prettier.format(content, {
|
||||
parser: 'typescript',
|
||||
filepath: filePath,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
endOfLine: 'lf',
|
||||
});
|
||||
fs.writeFileSync(filePath, formattedContent, 'utf-8');
|
||||
console.log(`✓ Generated ${filePath}`);
|
||||
};
|
||||
|
||||
const ensureDirectoriesExist = (): void => {
|
||||
if (!fs.existsSync(HOST_GENERATED_DIR)) {
|
||||
fs.mkdirSync(HOST_GENERATED_DIR, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(REMOTE_GENERATED_DIR)) {
|
||||
fs.mkdirSync(REMOTE_GENERATED_DIR, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
const main = (): void => {
|
||||
console.log('📖 Generating remote DOM elements...\n');
|
||||
|
||||
let htmlElements: ComponentSchema[];
|
||||
try {
|
||||
htmlElements = getHtmlElementSchemas();
|
||||
} catch (error) {
|
||||
console.error('❌ Validation failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`HTML Elements: ${htmlElements.length} elements`);
|
||||
console.log(
|
||||
` Tags: ${htmlElements.map((element) => element.htmlTag).join(', ')}`,
|
||||
);
|
||||
console.log(
|
||||
` Events: ${COMMON_HTML_EVENTS.length} common events per element`,
|
||||
);
|
||||
console.log('');
|
||||
|
||||
ensureDirectoriesExist();
|
||||
|
||||
const project = createProject();
|
||||
|
||||
console.log('Host files:');
|
||||
const hostRegistry = generateHostRegistry(
|
||||
project,
|
||||
htmlElements,
|
||||
EVENT_TO_REACT,
|
||||
);
|
||||
writeGeneratedFile(
|
||||
HOST_GENERATED_DIR,
|
||||
OUTPUT_FILES.HOST_REGISTRY,
|
||||
hostRegistry.getFullText(),
|
||||
);
|
||||
|
||||
console.log('\nRemote files:');
|
||||
const remoteElements = generateRemoteElements(
|
||||
project,
|
||||
htmlElements,
|
||||
HTML_COMMON_PROPERTIES,
|
||||
COMMON_HTML_EVENTS,
|
||||
);
|
||||
writeGeneratedFile(
|
||||
REMOTE_GENERATED_DIR,
|
||||
OUTPUT_FILES.REMOTE_ELEMENTS,
|
||||
remoteElements.getFullText(),
|
||||
);
|
||||
|
||||
const remoteComponents = generateRemoteComponents(project, htmlElements);
|
||||
writeGeneratedFile(
|
||||
REMOTE_GENERATED_DIR,
|
||||
OUTPUT_FILES.REMOTE_COMPONENTS,
|
||||
remoteComponents.getFullText(),
|
||||
);
|
||||
|
||||
console.log('\n✅ All generated files created');
|
||||
console.log(` Host: ${HOST_GENERATED_DIR}`);
|
||||
console.log(` Remote: ${REMOTE_GENERATED_DIR}`);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -1,4 +0,0 @@
|
||||
export const CUSTOM_ELEMENT_NAMES = {
|
||||
ROOT: 'remote-root',
|
||||
FRAGMENT: 'remote-fragment',
|
||||
} as const;
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export const INTERNAL_ELEMENT_CLASSES = {
|
||||
ROOT: 'RemoteRootElement',
|
||||
FRAGMENT: 'RemoteFragmentElement',
|
||||
} as const;
|
||||
@@ -1,5 +0,0 @@
|
||||
export const OUTPUT_FILES = {
|
||||
REMOTE_ELEMENTS: 'remote-elements.ts',
|
||||
REMOTE_COMPONENTS: 'remote-components.ts',
|
||||
HOST_REGISTRY: 'host-component-registry.ts',
|
||||
} as const;
|
||||
@@ -1,7 +0,0 @@
|
||||
export const TYPE_NAMES = {
|
||||
COMMON_PROPERTIES: 'HtmlCommonProperties',
|
||||
COMMON_EVENTS: 'HtmlCommonEvents',
|
||||
COMMON_PROPERTIES_CONFIG: 'HTML_COMMON_PROPERTIES_CONFIG',
|
||||
COMMON_EVENTS_ARRAY: 'HTML_COMMON_EVENTS_ARRAY',
|
||||
EMPTY_RECORD: 'Record<string, never>',
|
||||
} as const;
|
||||
@@ -1,4 +0,0 @@
|
||||
export { CUSTOM_ELEMENT_NAMES } from './CustomElementNames';
|
||||
export { INTERNAL_ELEMENT_CLASSES } from './InternalElementClasses';
|
||||
export { OUTPUT_FILES } from './OutputFiles';
|
||||
export { TYPE_NAMES } from './TypeNames';
|
||||
@@ -1,120 +0,0 @@
|
||||
import type { Project, SourceFile } from 'ts-morph';
|
||||
|
||||
import { CUSTOM_ELEMENT_NAMES } from './constants';
|
||||
import { type ComponentSchema } from './schemas';
|
||||
import { addFileHeader, addStatement } from './utils';
|
||||
|
||||
const generateRuntimeUtilities = (
|
||||
eventToReactMapping: Record<string, string>,
|
||||
): string => {
|
||||
const eventMapEntries = Object.entries(eventToReactMapping)
|
||||
.map(([domEvent, reactProp]) => ` on${domEvent}: '${reactProp}',`)
|
||||
.join('\n');
|
||||
|
||||
return `const INTERNAL_PROPS = new Set(['element', 'receiver', 'components']);
|
||||
|
||||
const EVENT_NAME_MAP: Record<string, string> = {
|
||||
${eventMapEntries}
|
||||
};
|
||||
|
||||
const parseStyle = (styleString: string | undefined): React.CSSProperties | undefined => {
|
||||
if (!styleString || typeof styleString !== 'string') {
|
||||
return styleString as React.CSSProperties | undefined;
|
||||
}
|
||||
|
||||
const style: Record<string, string> = {};
|
||||
const declarations = styleString.split(';').filter(Boolean);
|
||||
|
||||
for (const declaration of declarations) {
|
||||
const colonIndex = declaration.indexOf(':');
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const property = declaration.slice(0, colonIndex).trim();
|
||||
const value = declaration.slice(colonIndex + 1).trim();
|
||||
|
||||
const camelProperty = property.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
||||
style[camelProperty] = value;
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
const wrapEventHandler = (handler: () => void) => {
|
||||
return (_event: unknown) => {
|
||||
handler();
|
||||
};
|
||||
};
|
||||
|
||||
const filterProps = (props: Record<string, unknown>) => {
|
||||
const filtered: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (INTERNAL_PROPS.has(key) || value === undefined) continue;
|
||||
|
||||
if (key === 'style') {
|
||||
filtered.style = parseStyle(value as string | undefined);
|
||||
} else {
|
||||
const normalizedKey = EVENT_NAME_MAP[key.toLowerCase()] || key;
|
||||
if (normalizedKey.startsWith('on') && typeof value === 'function') {
|
||||
filtered[normalizedKey] = wrapEventHandler(value as () => void);
|
||||
} else {
|
||||
filtered[normalizedKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
};`;
|
||||
};
|
||||
|
||||
const generateWrapperComponent = (component: ComponentSchema): string => {
|
||||
return `const ${component.name}Wrapper = ({ children, ...props }: { children?: React.ReactNode } & Record<string, unknown>) => {
|
||||
return React.createElement('${component.htmlTag}', filterProps(props), children);
|
||||
};`;
|
||||
};
|
||||
|
||||
const generateRegistryMap = (components: ComponentSchema[]): string => {
|
||||
const entries = components
|
||||
.map(
|
||||
(component) =>
|
||||
` ['${component.customElementName}', createRemoteComponentRenderer(${component.name}Wrapper)],`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return `export const componentRegistry = new Map<string, ReturnType<typeof createRemoteComponentRenderer>>([
|
||||
${entries}
|
||||
['${CUSTOM_ELEMENT_NAMES.FRAGMENT}', RemoteFragmentRenderer],
|
||||
]);`;
|
||||
};
|
||||
|
||||
export const generateHostRegistry = (
|
||||
project: Project,
|
||||
components: ComponentSchema[],
|
||||
eventToReactMapping: Record<string, string>,
|
||||
): SourceFile => {
|
||||
const sourceFile = project.createSourceFile(
|
||||
'host-component-registry.ts',
|
||||
'',
|
||||
{ overwrite: true },
|
||||
);
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: 'react',
|
||||
defaultImport: 'React',
|
||||
});
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: '@remote-dom/react/host',
|
||||
namedImports: ['RemoteFragmentRenderer', 'createRemoteComponentRenderer'],
|
||||
});
|
||||
|
||||
addStatement(sourceFile, generateRuntimeUtilities(eventToReactMapping));
|
||||
|
||||
for (const component of components) {
|
||||
addStatement(sourceFile, generateWrapperComponent(component));
|
||||
}
|
||||
|
||||
addStatement(sourceFile, generateRegistryMap(components));
|
||||
|
||||
addFileHeader(sourceFile);
|
||||
|
||||
return sourceFile;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './constants';
|
||||
export { generateHostRegistry } from './host-registry.generator';
|
||||
export { generateRemoteComponents } from './remote-components.generator';
|
||||
export { generateRemoteElements } from './remote-elements.generator';
|
||||
export * from './schemas';
|
||||
export * from './utils';
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { Project, SourceFile } from 'ts-morph';
|
||||
|
||||
import { type ComponentSchema } from './schemas';
|
||||
import { addExportedConst, addFileHeader, eventToReactProp } from './utils';
|
||||
|
||||
const generateComponentDefinition = (
|
||||
sourceFile: SourceFile,
|
||||
component: ComponentSchema,
|
||||
): void => {
|
||||
const hasEvents = component.events.length > 0;
|
||||
const componentExportName = component.tagName;
|
||||
|
||||
let initializer: string;
|
||||
|
||||
if (hasEvents) {
|
||||
const eventProps = component.events
|
||||
.map((event) => {
|
||||
const propName = eventToReactProp(event);
|
||||
return ` ${propName}: { event: '${event}' },`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
initializer = `createRemoteComponent('${component.customElementName}', ${component.name}Element, {
|
||||
eventProps: {
|
||||
${eventProps}
|
||||
},
|
||||
})`;
|
||||
} else {
|
||||
initializer = `createRemoteComponent('${component.customElementName}', ${component.name}Element)`;
|
||||
}
|
||||
|
||||
addExportedConst(sourceFile, componentExportName, initializer);
|
||||
};
|
||||
|
||||
export const generateRemoteComponents = (
|
||||
project: Project,
|
||||
components: ComponentSchema[],
|
||||
): SourceFile => {
|
||||
const sourceFile = project.createSourceFile('remote-components.ts', '', {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: '@remote-dom/react',
|
||||
namedImports: ['createRemoteComponent'],
|
||||
});
|
||||
|
||||
const elementImports = components.map(
|
||||
(component) => `${component.name}Element`,
|
||||
);
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: './remote-elements',
|
||||
namedImports: elementImports,
|
||||
});
|
||||
|
||||
for (const component of components) {
|
||||
generateComponentDefinition(sourceFile, component);
|
||||
}
|
||||
|
||||
addFileHeader(sourceFile);
|
||||
|
||||
return sourceFile;
|
||||
};
|
||||
@@ -1,389 +0,0 @@
|
||||
import {
|
||||
type Project,
|
||||
type SourceFile,
|
||||
VariableDeclarationKind,
|
||||
type WriterFunction,
|
||||
} from 'ts-morph';
|
||||
|
||||
import {
|
||||
CUSTOM_ELEMENT_NAMES,
|
||||
INTERNAL_ELEMENT_CLASSES,
|
||||
TYPE_NAMES,
|
||||
} from './constants';
|
||||
import { type ComponentSchema, type PropertySchema } from './schemas';
|
||||
import {
|
||||
addFileHeader,
|
||||
generatePropertiesConfig,
|
||||
schemaTypeToConstructor,
|
||||
schemaTypeToTs,
|
||||
} from './utils';
|
||||
|
||||
type ElementGenerationOptions = {
|
||||
useSharedEvents: boolean;
|
||||
useSharedPropertiesConfig: boolean;
|
||||
};
|
||||
|
||||
type ComponentWithSpecificProps = {
|
||||
component: ComponentSchema;
|
||||
specificProperties: Record<string, PropertySchema>;
|
||||
};
|
||||
|
||||
const getElementSpecificProperties = (
|
||||
component: ComponentSchema,
|
||||
commonPropertyNames: Set<string>,
|
||||
): Record<string, PropertySchema> => {
|
||||
const specific: Record<string, PropertySchema> = {};
|
||||
for (const [name, schema] of Object.entries(component.properties)) {
|
||||
if (!commonPropertyNames.has(name)) {
|
||||
specific[name] = schema;
|
||||
}
|
||||
}
|
||||
return specific;
|
||||
};
|
||||
|
||||
const generatePropertyEntries = (
|
||||
properties: Record<string, PropertySchema>,
|
||||
): string[] => {
|
||||
return Object.entries(properties).map(([name, schema]) => {
|
||||
const tsType = schemaTypeToTs(schema.type);
|
||||
const optional = schema.optional ? '?' : '';
|
||||
return `'${name}'${optional}: ${tsType}`;
|
||||
});
|
||||
};
|
||||
|
||||
const generateCommonPropertiesType = (
|
||||
sourceFile: SourceFile,
|
||||
commonProperties: Record<string, PropertySchema>,
|
||||
): void => {
|
||||
const entries = generatePropertyEntries(commonProperties);
|
||||
|
||||
sourceFile.addTypeAlias({
|
||||
isExported: true,
|
||||
name: TYPE_NAMES.COMMON_PROPERTIES,
|
||||
type: (writer) => {
|
||||
writer.block(() => {
|
||||
for (const entry of entries) {
|
||||
writer.writeLine(`${entry};`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const generateCommonEventsType = (
|
||||
sourceFile: SourceFile,
|
||||
events: readonly string[],
|
||||
): void => {
|
||||
if (events.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sourceFile.addTypeAlias({
|
||||
isExported: true,
|
||||
name: TYPE_NAMES.COMMON_EVENTS,
|
||||
type: (writer) => {
|
||||
writer.block(() => {
|
||||
for (const event of events) {
|
||||
writer.writeLine(`${event}(event: RemoteEvent): void;`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
sourceFile.addVariableStatement({
|
||||
declarationKind: VariableDeclarationKind.Const,
|
||||
declarations: [
|
||||
{
|
||||
name: TYPE_NAMES.COMMON_EVENTS_ARRAY,
|
||||
initializer: (writer) => {
|
||||
writer.write('[');
|
||||
writer.newLine();
|
||||
writer.indent(() => {
|
||||
for (const event of events) {
|
||||
writer.writeLine(`'${event}',`);
|
||||
}
|
||||
});
|
||||
writer.write('] as const');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const generateCommonPropertiesConfig = (
|
||||
sourceFile: SourceFile,
|
||||
commonProperties: Record<string, PropertySchema>,
|
||||
): void => {
|
||||
sourceFile.addVariableStatement({
|
||||
declarationKind: VariableDeclarationKind.Const,
|
||||
declarations: [
|
||||
{
|
||||
name: TYPE_NAMES.COMMON_PROPERTIES_CONFIG,
|
||||
initializer: (writer) => {
|
||||
writer.block(() => {
|
||||
for (const [name, schema] of Object.entries(commonProperties)) {
|
||||
const constructorType = schemaTypeToConstructor(schema.type);
|
||||
writer.writeLine(`'${name}': { type: ${constructorType} },`);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const generateElementPropertyType = (
|
||||
sourceFile: SourceFile,
|
||||
component: ComponentSchema,
|
||||
specificProperties: Record<string, PropertySchema>,
|
||||
): void => {
|
||||
const hasSpecificProps = Object.keys(specificProperties).length > 0;
|
||||
|
||||
if (hasSpecificProps) {
|
||||
const entries = generatePropertyEntries(specificProperties);
|
||||
sourceFile.addTypeAlias({
|
||||
isExported: true,
|
||||
name: `${component.name}Properties`,
|
||||
type: (writer) => {
|
||||
writer.write(`${TYPE_NAMES.COMMON_PROPERTIES} & `);
|
||||
writer.block(() => {
|
||||
for (const entry of entries) {
|
||||
writer.writeLine(`${entry};`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createPropertiesConfigWriter = (
|
||||
properties: Record<string, PropertySchema>,
|
||||
): WriterFunction => {
|
||||
return (writer) => {
|
||||
writer.write(generatePropertiesConfig(properties));
|
||||
};
|
||||
};
|
||||
|
||||
const generateElementDefinition = (
|
||||
sourceFile: SourceFile,
|
||||
component: ComponentSchema,
|
||||
specificProperties: Record<string, PropertySchema>,
|
||||
options: ElementGenerationOptions,
|
||||
): void => {
|
||||
const { useSharedEvents, useSharedPropertiesConfig } = options;
|
||||
const hasEvents = component.events.length > 0;
|
||||
const hasSpecificProps = Object.keys(specificProperties).length > 0;
|
||||
const hasProps = Object.keys(component.properties).length > 0;
|
||||
|
||||
const propsType = hasSpecificProps
|
||||
? `${component.name}Properties`
|
||||
: hasProps
|
||||
? TYPE_NAMES.COMMON_PROPERTIES
|
||||
: TYPE_NAMES.EMPTY_RECORD;
|
||||
|
||||
const eventsType = hasEvents
|
||||
? useSharedEvents
|
||||
? TYPE_NAMES.COMMON_EVENTS
|
||||
: `{ ${component.events.map((event) => `${event}(event: RemoteEvent): void`).join('; ')} }`
|
||||
: TYPE_NAMES.EMPTY_RECORD;
|
||||
|
||||
sourceFile.addVariableStatement({
|
||||
isExported: true,
|
||||
declarationKind: VariableDeclarationKind.Const,
|
||||
declarations: [
|
||||
{
|
||||
name: `${component.name}Element`,
|
||||
initializer: (writer) => {
|
||||
writer.write('createRemoteElement<');
|
||||
writer.newLine();
|
||||
writer.indent(() => {
|
||||
writer.writeLine(`${propsType},`);
|
||||
writer.writeLine('Record<string, never>,');
|
||||
writer.writeLine('Record<string, never>,');
|
||||
writer.write(eventsType);
|
||||
});
|
||||
writer.newLine();
|
||||
writer.write('>');
|
||||
|
||||
const hasConfig = hasProps || hasEvents;
|
||||
if (!hasConfig) {
|
||||
writer.write('({})');
|
||||
return;
|
||||
}
|
||||
|
||||
writer.write('(');
|
||||
writer.block(() => {
|
||||
if (hasProps) {
|
||||
if (hasSpecificProps) {
|
||||
writer.write('properties: ');
|
||||
writer.block(() => {
|
||||
writer.writeLine(
|
||||
`...${TYPE_NAMES.COMMON_PROPERTIES_CONFIG},`,
|
||||
);
|
||||
for (const [name, schema] of Object.entries(
|
||||
specificProperties,
|
||||
)) {
|
||||
const constructorType = schemaTypeToConstructor(
|
||||
schema.type,
|
||||
);
|
||||
writer.writeLine(
|
||||
`'${name}': { type: ${constructorType} },`,
|
||||
);
|
||||
}
|
||||
});
|
||||
writer.write(',');
|
||||
writer.newLine();
|
||||
} else if (useSharedPropertiesConfig) {
|
||||
writer.write(
|
||||
`properties: ${TYPE_NAMES.COMMON_PROPERTIES_CONFIG},`,
|
||||
);
|
||||
writer.newLine();
|
||||
} else {
|
||||
writer.write('properties: ');
|
||||
createPropertiesConfigWriter(component.properties)(writer);
|
||||
writer.write(',');
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
if (hasEvents) {
|
||||
writer.write(
|
||||
useSharedEvents
|
||||
? `events: [...${TYPE_NAMES.COMMON_EVENTS_ARRAY}],`
|
||||
: `events: [${component.events.map((event) => `'${event}'`).join(', ')}],`,
|
||||
);
|
||||
writer.newLine();
|
||||
}
|
||||
});
|
||||
writer.write(')');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const generateCustomElementRegistrations = (
|
||||
sourceFile: SourceFile,
|
||||
components: ComponentSchema[],
|
||||
): void => {
|
||||
for (const component of components) {
|
||||
sourceFile.addStatements(
|
||||
`customElements.define('${component.customElementName}', ${component.name}Element);`,
|
||||
);
|
||||
}
|
||||
sourceFile.addStatements(
|
||||
`customElements.define('${CUSTOM_ELEMENT_NAMES.ROOT}', ${INTERNAL_ELEMENT_CLASSES.ROOT});`,
|
||||
);
|
||||
sourceFile.addStatements(
|
||||
`customElements.define('${CUSTOM_ELEMENT_NAMES.FRAGMENT}', ${INTERNAL_ELEMENT_CLASSES.FRAGMENT});`,
|
||||
);
|
||||
};
|
||||
|
||||
const generateTagNameMapDeclaration = (
|
||||
sourceFile: SourceFile,
|
||||
components: ComponentSchema[],
|
||||
): void => {
|
||||
sourceFile.addStatements((writer) => {
|
||||
writer.writeLine('declare global {');
|
||||
writer.indent(() => {
|
||||
writer.writeLine('interface HTMLElementTagNameMap {');
|
||||
writer.indent(() => {
|
||||
for (const component of components) {
|
||||
writer.writeLine(
|
||||
`'${component.customElementName}': InstanceType<typeof ${component.name}Element>;`,
|
||||
);
|
||||
}
|
||||
writer.writeLine(
|
||||
`'${CUSTOM_ELEMENT_NAMES.ROOT}': InstanceType<typeof ${INTERNAL_ELEMENT_CLASSES.ROOT}>;`,
|
||||
);
|
||||
writer.writeLine(
|
||||
`'${CUSTOM_ELEMENT_NAMES.FRAGMENT}': InstanceType<typeof ${INTERNAL_ELEMENT_CLASSES.FRAGMENT}>;`,
|
||||
);
|
||||
});
|
||||
writer.writeLine('}');
|
||||
});
|
||||
writer.writeLine('}');
|
||||
});
|
||||
};
|
||||
|
||||
const prepareComponentsWithSpecificProps = (
|
||||
components: ComponentSchema[],
|
||||
commonPropertyNames: Set<string>,
|
||||
): ComponentWithSpecificProps[] => {
|
||||
return components.map((component) => ({
|
||||
component,
|
||||
specificProperties: getElementSpecificProperties(
|
||||
component,
|
||||
commonPropertyNames,
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
export const generateRemoteElements = (
|
||||
project: Project,
|
||||
components: ComponentSchema[],
|
||||
commonProperties: Record<string, PropertySchema>,
|
||||
commonEvents: readonly string[] = [],
|
||||
): SourceFile => {
|
||||
const sourceFile = project.createSourceFile('remote-elements.ts', '', {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
const useSharedEvents = commonEvents.length > 0;
|
||||
const useSharedPropertiesConfig = Object.keys(commonProperties).length > 0;
|
||||
|
||||
const options: ElementGenerationOptions = {
|
||||
useSharedEvents,
|
||||
useSharedPropertiesConfig,
|
||||
};
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: '@remote-dom/core/elements',
|
||||
namedImports: [
|
||||
'createRemoteElement',
|
||||
INTERNAL_ELEMENT_CLASSES.ROOT,
|
||||
INTERNAL_ELEMENT_CLASSES.FRAGMENT,
|
||||
{ name: 'RemoteEvent', isTypeOnly: true },
|
||||
],
|
||||
});
|
||||
|
||||
const commonPropertyNames = new Set(Object.keys(commonProperties));
|
||||
const componentsWithProps = prepareComponentsWithSpecificProps(
|
||||
components,
|
||||
commonPropertyNames,
|
||||
);
|
||||
|
||||
generateCommonPropertiesType(sourceFile, commonProperties);
|
||||
|
||||
if (useSharedEvents) {
|
||||
generateCommonEventsType(sourceFile, commonEvents);
|
||||
}
|
||||
|
||||
if (useSharedPropertiesConfig) {
|
||||
generateCommonPropertiesConfig(sourceFile, commonProperties);
|
||||
}
|
||||
|
||||
for (const { component, specificProperties } of componentsWithProps) {
|
||||
generateElementPropertyType(sourceFile, component, specificProperties);
|
||||
generateElementDefinition(
|
||||
sourceFile,
|
||||
component,
|
||||
specificProperties,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
generateCustomElementRegistrations(sourceFile, components);
|
||||
|
||||
sourceFile.addExportDeclaration({
|
||||
namedExports: [
|
||||
INTERNAL_ELEMENT_CLASSES.ROOT,
|
||||
INTERNAL_ELEMENT_CLASSES.FRAGMENT,
|
||||
],
|
||||
});
|
||||
|
||||
generateTagNameMapDeclaration(sourceFile, components);
|
||||
|
||||
addFileHeader(sourceFile);
|
||||
|
||||
return sourceFile;
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PropertySchemaZ = z.object({
|
||||
type: z.enum(['string', 'number', 'boolean']),
|
||||
optional: z.boolean(),
|
||||
});
|
||||
|
||||
export const HtmlElementConfigZ = z.object({
|
||||
tag: z.string().regex(/^html-[a-z0-9]+$/, 'Tag must start with "html-"'),
|
||||
name: z
|
||||
.string()
|
||||
.regex(/^Html[A-Z]/, 'Name must be PascalCase starting with Html'),
|
||||
properties: z.record(z.string(), PropertySchemaZ),
|
||||
});
|
||||
|
||||
export const HtmlElementConfigArrayZ = z.array(HtmlElementConfigZ);
|
||||
|
||||
export const ComponentSchemaZ = z.object({
|
||||
name: z.string().min(1),
|
||||
tagName: z.string().min(1),
|
||||
customElementName: z.string().min(1),
|
||||
properties: z.record(z.string(), PropertySchemaZ),
|
||||
events: z.array(z.string()).readonly(),
|
||||
isHtmlElement: z.boolean(),
|
||||
htmlTag: z.string().min(1),
|
||||
});
|
||||
|
||||
export type PropertySchema = z.infer<typeof PropertySchemaZ>;
|
||||
export type HtmlElementConfig = z.infer<typeof HtmlElementConfigZ>;
|
||||
export type ComponentSchema = z.infer<typeof ComponentSchemaZ>;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { type SourceFile, VariableDeclarationKind } from 'ts-morph';
|
||||
|
||||
export const addExportedConst = (
|
||||
sourceFile: SourceFile,
|
||||
name: string,
|
||||
initializer: string,
|
||||
): void => {
|
||||
sourceFile.addVariableStatement({
|
||||
isExported: true,
|
||||
declarationKind: VariableDeclarationKind.Const,
|
||||
declarations: [{ name, initializer }],
|
||||
});
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { type SourceFile } from 'ts-morph';
|
||||
|
||||
export const addExportedType = (
|
||||
sourceFile: SourceFile,
|
||||
name: string,
|
||||
type: string,
|
||||
): void => {
|
||||
sourceFile.addTypeAlias({
|
||||
isExported: true,
|
||||
name,
|
||||
type,
|
||||
});
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { type SourceFile } from 'ts-morph';
|
||||
|
||||
import { GENERATED_FILE_HEADER } from './generated-file-header';
|
||||
|
||||
export const addFileHeader = (sourceFile: SourceFile): void => {
|
||||
sourceFile.insertText(0, GENERATED_FILE_HEADER + '\n\n');
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { type SourceFile } from 'ts-morph';
|
||||
|
||||
export const addStatement = (
|
||||
sourceFile: SourceFile,
|
||||
statement: string,
|
||||
): void => {
|
||||
sourceFile.addStatements(statement);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const eventToReactProp = (eventName: string): string => {
|
||||
return `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const extractHtmlTag = (tag: string): string => {
|
||||
return tag.startsWith('html-') ? tag.slice(5) : tag;
|
||||
};
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { type PropertySchema } from '../schemas';
|
||||
import { schemaTypeToConstructor } from './schema-type-to-constructor';
|
||||
|
||||
export const generatePropertiesConfig = (
|
||||
properties: Record<string, PropertySchema>,
|
||||
): string => {
|
||||
const entries = Object.entries(properties);
|
||||
if (entries.length === 0) {
|
||||
return '{}';
|
||||
}
|
||||
|
||||
const props = entries
|
||||
.map(([name, schema]) => {
|
||||
const constructorType = schemaTypeToConstructor(schema.type);
|
||||
return `'${name}': { type: ${constructorType} }`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
return `{ ${props} }`;
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { type PropertySchema } from '../schemas';
|
||||
import { schemaTypeToTs } from './schema-type-to-ts';
|
||||
|
||||
export const generatePropertiesType = (
|
||||
properties: Record<string, PropertySchema>,
|
||||
): string => {
|
||||
const entries = Object.entries(properties);
|
||||
if (entries.length === 0) {
|
||||
return 'Record<string, never>';
|
||||
}
|
||||
|
||||
const props = entries
|
||||
.map(([name, schema]) => {
|
||||
const tsType = schemaTypeToTs(schema.type);
|
||||
const optional = schema.optional ? '?' : '';
|
||||
return `'${name}'${optional}: ${tsType}`;
|
||||
})
|
||||
.join('; ');
|
||||
|
||||
return `{ ${props} }`;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
export const GENERATED_FILE_HEADER = `/*
|
||||
* _____ _
|
||||
*|_ _|_ _____ _ __ | |_ _ _
|
||||
* | | \\ \\ /\\ / / _ \\ '_ \\| __| | | | Auto-generated file
|
||||
* | | \\ V V / __/ | | | |_| |_| | Any edits to this will be overridden
|
||||
* |_| \\_/\\_/ \\___|_| |_|\\__|\\__, |
|
||||
* |___/
|
||||
*/`;
|
||||
@@ -1,11 +0,0 @@
|
||||
export { addExportedConst } from './add-exported-const';
|
||||
export { addExportedType } from './add-exported-type';
|
||||
export { addFileHeader } from './add-file-header';
|
||||
export { addStatement } from './add-statement';
|
||||
export { eventToReactProp } from './event-to-react-prop';
|
||||
export { extractHtmlTag } from './extract-html-tag';
|
||||
export { GENERATED_FILE_HEADER } from './generated-file-header';
|
||||
export { generatePropertiesConfig } from './generate-properties-config';
|
||||
export { generatePropertiesType } from './generate-properties-type';
|
||||
export { schemaTypeToConstructor } from './schema-type-to-constructor';
|
||||
export { schemaTypeToTs } from './schema-type-to-ts';
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { type PropertySchema } from '../schemas';
|
||||
|
||||
const SCHEMA_TYPE_TO_CONSTRUCTOR: Record<PropertySchema['type'], string> = {
|
||||
boolean: 'Boolean',
|
||||
number: 'Number',
|
||||
string: 'String',
|
||||
};
|
||||
|
||||
export const schemaTypeToConstructor = (type: PropertySchema['type']): string =>
|
||||
SCHEMA_TYPE_TO_CONSTRUCTOR[type];
|
||||
@@ -1,10 +0,0 @@
|
||||
import { type PropertySchema } from '../schemas';
|
||||
|
||||
const SCHEMA_TYPE_TO_TS: Record<PropertySchema['type'], string> = {
|
||||
boolean: 'boolean',
|
||||
number: 'number',
|
||||
string: 'string',
|
||||
};
|
||||
|
||||
export const schemaTypeToTs = (type: PropertySchema['type']): string =>
|
||||
SCHEMA_TYPE_TO_TS[type];
|
||||
Reference in New Issue
Block a user