UNPKG

@kform/scaffolder

Version:

Scaffolding utilities for KForm projects.

1,440 lines 57.3 kB
import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { useDroppable, useDraggable, useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter, DragOverlay } from "@dnd-kit/core"; import * as React from "react"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import { fileOpen, fileSave, supported } from "browser-fs-access"; import JSZip from "jszip"; import { pascalCase, camelCase } from "change-case"; import { render } from "ejs"; function createSchematic(schematic) { return { id: self.crypto.randomUUID(), kind: "", ...schematic }; } function moveSchematic(schematic, moveId, parentId, sortBefore) { const { schematic: schematicWithoutFrom, removed: fromSchematic } = remove( schematic, moveId ); return add(schematicWithoutFrom, fromSchematic, parentId, sortBefore); } function remove(schematic, id) { if (schematic.id === id) { return { schematic: null, removed: schematic }; } if (!schematic.children) { return { schematic, removed: null }; } const newChildren = []; let removed = null; for (const child of schematic.children) { const { schematic: newChild, removed: removedChild } = remove(child, id); if (newChild !== null) { newChildren.push(newChild); } if (removedChild !== null) { removed = removedChild; } } return { schematic: { ...schematic, children: newChildren }, removed }; } function add(schematic, toAdd, parentId, sortBefore) { if (schematic.id === parentId) { const oldChildren = schematic.children ?? []; const index = sortBefore === null ? oldChildren.length : oldChildren.findIndex((c) => c.id === sortBefore); return { ...schematic, children: [ ...oldChildren.slice(0, index), toAdd, ...oldChildren.slice(index) ] }; } if (!schematic.children) { return schematic; } return { ...schematic, children: schematic.children.map( (child) => add(child, toAdd, parentId, sortBefore) ) }; } const SchematicBuilderContext = React.createContext(null); function useSchematicBuilderContext() { const context = React.useContext(SchematicBuilderContext); if (context === null) { throw new Error("Schematic builder not in context."); } return context; } function LoadSchematic() { const { name, setSchematic, latestSchematicFileHandle } = useSchematicBuilderContext(); return /* @__PURE__ */ jsx( "button", { className: "builder-icon-button builder-action builder-load-schematic", type: "button", onClick: async () => { try { const file = await fileOpen({ extensions: [".json"], mimeTypes: ["application/json"], description: "Form schematic", id: name }); if (file.handle) { latestSchematicFileHandle.current = file.handle; } const content = JSON.parse(await file.text()); setSchematic(() => content); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { return; } alert(`Unable to load schematic: ${err?.toString()}`); } }, title: "Load schematic", "aria-label": "Load schematic", children: "📂" } ); } function SaveSchematic() { const { name, getRootSchematic, latestSchematicFileHandle } = useSchematicBuilderContext(); return /* @__PURE__ */ jsx( "button", { className: "builder-icon-button builder-action builder-save-schematic", type: "button", onClick: async () => { try { const schematic = getRootSchematic(); latestSchematicFileHandle.current = await fileSave( new Blob([JSON.stringify(schematic, null, 2)]), { fileName: `${schematic.name}-schematic.json`, extensions: [".json"], mimeTypes: ["application/json"], description: "Form schematic", id: name }, latestSchematicFileHandle.current ); if (supported) { alert( `Schematic saved successfully as “${latestSchematicFileHandle.current.name}”.` ); } } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { return; } alert(`Unable to save schematic: ${err?.toString()}`); } }, title: "Save schematic", "aria-label": "Save schematic", children: "💾" } ); } function configScaffolder(scaffolder, data) { return (schematic, originalData) => scaffolder(schematic, { ...originalData, ...data }); } function join(s1, s2, separator) { return !s1 || !s2 ? s1 || s2 || "" : `${s1}${s1.endsWith(separator) ? "" : separator}${s2}`; } function joinPaths(path1, path2) { return join(path1, path2, "/"); } function joinPackages(pkg1, pkg2) { return join(pkg1, pkg2, "."); } function scaffold(schematic, scaffolders, options) { const files = /* @__PURE__ */ new Map(); const data = { schematicKinds: options.schematicKinds, rootSchematic: schematic, files, currentPath: options.basePath ?? "/", currentPackage: joinPackages(options.basePackage, schematic.packageSuffix), currentDir: options.baseDir }; for (const scaffolder of scaffolders) { scaffolder(schematic, data); } return Array.from(files).map(([path, file]) => ({ path, content: file.getContent(), base64: file.base64, binary: file.binary, executable: file.executable })); } function useScaffolder() { const { basePath, basePackage, baseDir, schematicKinds, scaffolders, scaffoldingData, getRootSchematic } = useSchematicBuilderContext(); return React.useCallback(() => { const schematic = getRootSchematic(); let actualScaffolders = typeof scaffolders === "function" ? scaffolders(schematic) : scaffolders; const actualScaffoldingData = typeof scaffoldingData === "function" ? scaffoldingData(schematic) : scaffoldingData; if (actualScaffoldingData) { actualScaffolders = actualScaffolders.map( (scaffolder) => configScaffolder(scaffolder, actualScaffoldingData) ); } return scaffold(schematic, actualScaffolders, { schematicKinds, basePath, basePackage, baseDir: typeof baseDir === "function" ? baseDir(schematic) : baseDir }); }, [ baseDir, basePackage, basePath, getRootSchematic, scaffolders, scaffoldingData, schematicKinds ]); } let latestZipFileHandle = null; function ScaffoldToZip() { const { getRootSchematic, name } = useSchematicBuilderContext(); const scaffold2 = useScaffolder(); return /* @__PURE__ */ jsx( "button", { className: "builder-input builder-action builder-scaffold-zip", type: "submit", onClick: async (evt) => { evt.preventDefault(); try { const schematic = getRootSchematic(); const files = scaffold2(); const zip = new JSZip(); for (const { path, content, base64, binary, executable } of files) { zip.file(path, content, { base64, binary, unixPermissions: executable ? "755" : void 0 }); } latestZipFileHandle = await fileSave( zip.generateAsync({ platform: "UNIX", type: "blob" }), { fileName: `${schematic.name}.zip`, extensions: [".zip"], mimeTypes: ["application/zip"], description: "Form scaffolding", id: name }, latestZipFileHandle ); if (supported) { alert( `Project scaffolded successfully in “${latestZipFileHandle.name}”.` ); } } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { return; } alert(`Unable to scaffold project: ${err?.toString()}`); } }, children: "Scaffold 🏗️" } ); } function useConfig(configName, defaultValue) { const { useSchematic, setSchematic } = useSchematicBuilderContext(); const configValue = useSchematic( (schematic) => schematic.config?.[configName] ?? defaultValue ); const setConfigValue = React.useCallback( (configValue2) => setSchematic((schematic) => ({ config: { ...schematic.config, [configName]: typeof configValue2 === "function" ? configValue2(schematic.config?.[configName]) : configValue2 } })), [configName, setSchematic] ); return React.useMemo( () => [configValue, setConfigValue], [configValue, setConfigValue] ); } function UseFileBase64SerializerConfig({ disabled }) { const [config, setConfig] = useConfig("useFileBase64Serializer", false); return /* @__PURE__ */ jsx("div", { className: "builder-config-field", children: /* @__PURE__ */ jsxs("label", { children: [ /* @__PURE__ */ jsx( "input", { type: "checkbox", checked: config, onChange: (evt) => setConfig(evt.target.checked), disabled } ), "Use ", /* @__PURE__ */ jsx("code", { children: "File.Base64Serializer" }) ] }) }); } function UseTableValuesSerializerConfig({ disabled }) { const [config, setConfig] = useConfig("useTableValuesSerializer", false); return /* @__PURE__ */ jsx("div", { className: "builder-config-field", children: /* @__PURE__ */ jsxs("label", { children: [ /* @__PURE__ */ jsx( "input", { type: "checkbox", checked: config, onChange: (evt) => setConfig(evt.target.checked), disabled } ), "Use ", /* @__PURE__ */ jsx("code", { children: "Table.ValuesSerializer" }) ] }) }); } const DEFAULT_IMPORTS = [ /^kotlin\.[^.]+$/, /^kotlin\.annotation\.[^.]+$/, /^kotlin\.collections\.[^.]+$/, /^kotlin\.comparisons\.[^.]+$/, /^kotlin\.io\.[^.]+$/, /^kotlin\.ranges\.[^.]+$/, /^kotlin\.sequences\.[^.]+$/, /^kotlin\.text\.[^.]+$/ ]; function isDefaultKtImport(qualifiedName) { return DEFAULT_IMPORTS.some((i) => i.test(qualifiedName)); } function ktFile(data) { return { package: data.currentPackage ?? "", imports: /* @__PURE__ */ new Set(), declarations: [], getContent: ktFileContent }; } function ktFileContent() { return [ // Package this.package && `package ${this.package}`, // Imports Array.from(this.imports).filter((i) => !isDefaultKtImport(i)).sort().map((i) => `import ${i}`).join("\n"), // Declarations ...this.declarations ].filter((s) => s).join("\n\n") + "\n"; } function simpleKtName(qualifiedName) { return qualifiedName.substring(qualifiedName.lastIndexOf(".") + 1); } const JS_EXPORT = "kotlin.js.JsExport"; const SERIALIZABLE = "kotlinx.serialization.Serializable"; function annotate(annotations, code2) { if (!annotations || annotations.length === 0) { return code2; } return [ ...typeof annotations === "string" ? [annotations] : annotations, code2 ].join("\n"); } function jsExport(code2, data) { data.currentFile.imports.add(JS_EXPORT); return annotate(`@${simpleKtName(JS_EXPORT)}`, code2); } function serializable(code2, data) { data.currentFile.imports.add(SERIALIZABLE); return annotate(`@${simpleKtName(SERIALIZABLE)}`, code2); } function scaffoldModels(schematic, data) { const fileName = joinPaths(data.currentDir, `${schematic.name}.kt`); let file = data.files.get(fileName); if (!file) { data.files.set(fileName, file = ktFile(data)); } scaffoldModelDeclaration(schematic, { ...data, currentFile: file }); } function scaffoldModelDeclaration(schematic, data) { const schematicKind = data.schematicKinds.get(schematic.kind); if (!schematicKind) { throw new Error(`Unknown schematic kind ${schematic.kind}`); } const nDecls = data.currentFile.declarations.length; data.currentFile.declarations.push(""); const decl = schematicKind.scaffoldModel?.(schematic, data) ?? `typealias ${schematic.name} = ${scaffoldType(schematic, data)}`; data.currentFile.declarations[nDecls] = schematicKind.scaffoldModel ? jsExport(serializable(decl, data), data) : decl; } function scaffoldPropertyAnnotations(schematic, data) { const schematicKind = data.schematicKinds.get(schematic.kind); if (!schematicKind) { throw new Error(`Unknown schematic kind ${schematic.kind}`); } return typeof schematicKind.propertyAnnotations === "function" ? schematicKind.propertyAnnotations(schematic, data) : schematicKind.propertyAnnotations; } function scaffoldType(schematic, data) { const schematicKind = data.schematicKinds.get(schematic.kind); if (!schematicKind) { throw new Error(`Unknown schematic kind ${schematic.kind}`); } const schematicPackage = schematicKind.package ?? joinPackages(data.currentPackage, schematic.packageSuffix); const schematicName = schematic.name || schematicKind.name; if (schematicPackage !== data.currentPackage) { data.currentFile.imports.add(joinPackages(schematicPackage, schematicName)); } if (schematicKind.package == null && schematicPackage !== data.currentPackage) { scaffoldModels(schematic, { ...data, currentPackage: joinPackages( data.currentPackage, schematic.packageSuffix ), currentDir: joinPaths( data.currentDir, schematic.packageSuffix?.replace(".", "/") ) }); } else if (schematicKind.scaffoldModel) { scaffoldModelDeclaration(schematic, data); } return (schematicKind.scaffoldType?.(schematic, data) ?? schematicName) + (schematic.nullable ? "?" : ""); } function scaffoldDefaultValue(schematic, data) { const schematicKind = data.schematicKinds.get(schematic.kind); if (!schematicKind) { throw new Error(`Unknown schematic kind ${schematic.kind}`); } return schematic.nullable ? "null" : typeof schematicKind.defaultValue === "function" ? schematicKind.defaultValue(schematic, data) : schematicKind.defaultValue; } function boolDataAttr(condition) { return condition ? "" : void 0; } const preventDrag = { onKeyDown: (evt) => evt.stopPropagation(), onPointerDown: (evt) => evt.stopPropagation() }; function ChildMarker() { const { schematicKinds, useSchematic, setSchematic } = useSchematicBuilderContext(); const kind = useSchematic((schematic) => schematic.kind); const collapsed = useSchematic((schematic) => schematic.collapsed); const Builder = schematicKinds.get(kind)?.builder; return Builder ? /* @__PURE__ */ jsx( "button", { type: "button", className: "builder-child-marker", title: collapsed ? "Expand" : "Collapse", onClick: () => setSchematic((schematic) => ({ collapsed: !schematic.collapsed })), ...preventDrag, children: collapsed ? "▸" : "▾" } ) : /* @__PURE__ */ jsx("span", { className: "builder-child-marker", children: "•" }); } function ChildRemove() { const { removeSchematic } = useSchematicBuilderContext(); return /* @__PURE__ */ jsx( "button", { className: "builder-icon-button builder-child-remove", type: "button", title: "Remove", onClick: removeSchematic, ...preventDrag, children: "×" } ); } const KIND_PLACEHOLDER = "--KIND--"; function KindSelect() { const { schematicKinds, useSchematic, setSchematic, parentChildName } = useSchematicBuilderContext(); const kind = useSchematic((schematic) => schematic.kind); return /* @__PURE__ */ jsxs( "select", { className: "builder-input builder-schema", value: kind, onChange: (evt) => { const schematicKind = schematicKinds.get(evt.target.value); setSchematic({ kind: evt.target.value, packageSuffix: schematicKind?.defaultPackageSuffix?.( parentChildName ), name: schematicKind?.defaultName?.(parentChildName), collapsed: false, nullable: schematicKind?.nullable ?? schematicKind?.defaultNullable, children: schematicKind?.initChildren?.(parentChildName) }); }, required: true, style: { width: `calc(${(kind || KIND_PLACEHOLDER).length}ch + 1.85rem)` }, ...preventDrag, children: [ /* @__PURE__ */ jsx("option", { children: KIND_PLACEHOLDER }), Array.from(schematicKinds.values()).filter((kind2) => !kind2.internal).map((kind2) => /* @__PURE__ */ jsx("option", { value: kind2.kind, children: kind2.kind }, kind2.kind)) ] } ); } function NullableInput() { const { schematicKinds, useSchematic, setSchematic } = useSchematicBuilderContext(); const kind = useSchematic((schematic) => schematic.kind); const nullable = useSchematic((schematic) => !!schematic.nullable); const schematicKind = schematicKinds.get(kind); const showInput = schematicKind && schematicKind.nullable === void 0; return showInput && /* @__PURE__ */ jsx( "input", { className: "builder-input builder-nullable", type: "checkbox", title: "Nullable?", checked: nullable, onChange: (evt) => setSchematic({ nullable: evt.target.checked }), ...preventDrag } ); } const PACKAGE_PLACEHOLDER = "package.suffix"; const NAME_PLACEHOLDER = "ClassName"; function PackageNameInput() { const { useSchematic, setSchematic, parentPackage } = useSchematicBuilderContext(); const pkgRef = React.useRef(null); const packageSuffix = useSchematic((schematic) => schematic.packageSuffix); const name = useSchematic((schematic) => schematic.name); return /* @__PURE__ */ jsxs("div", { className: "builder-package-name", children: [ parentPackage && /* @__PURE__ */ jsxs("span", { className: "builder-package-prefix", children: [ parentPackage, "." ] }), /* @__PURE__ */ jsx( "input", { className: "builder-input builder-package", placeholder: PACKAGE_PLACEHOLDER, value: packageSuffix ?? "", onChange: (evt) => setSchematic({ packageSuffix: evt.target.value }), pattern: "(([a-z_][a-z0-9_]*)(\\.([a-z_][a-z0-9_]*))*)?", style: { width: `${(packageSuffix || PACKAGE_PLACEHOLDER).length}ch` }, ...preventDrag, ref: pkgRef } ), /* @__PURE__ */ jsx( "button", { type: "button", className: "builder-icon-button builder-edit-package", onClick: () => { const pkgInput = pkgRef.current; pkgInput.style.display = "initial"; pkgInput.focus(); pkgInput.style.display = ""; }, children: "🖉" } ), /* @__PURE__ */ jsx("span", { className: "builder-operator", children: "." }), /* @__PURE__ */ jsx( "input", { className: "builder-input builder-name", placeholder: NAME_PLACEHOLDER, value: name ?? "", onChange: (evt) => setSchematic({ name: evt.target.value }), required: true, pattern: "[a-zA-Z_][a-zA-Z0-9_]*", style: { width: `${(name || NAME_PLACEHOLDER).length}ch` }, ...preventDrag } ) ] }); } const CLASS_DND_TYPE = "class-field"; function ClassBuilder() { const schematicBuilderContext = useSchematicBuilderContext(); const { useSchematic, setSchematic, draggedSchematic, disableDrop } = schematicBuilderContext; const id = useSchematic((schematic) => schematic.id); const packageSuffix = useSchematic((schematic) => schematic.packageSuffix); const fields = useSchematic((schematic) => schematic.children) ?? []; const shouldIgnoreDrop = draggedSchematic != null && draggedSchematic.id === fields.at(-1)?.id; const { setNodeRef: setDroppableRef, isOver } = useDroppable({ id: `${id}-append`, data: { parentId: id, sortBefore: null, ignored: shouldIgnoreDrop }, disabled: disableDrop || draggedSchematic?.type !== CLASS_DND_TYPE }); return /* @__PURE__ */ jsxs("div", { className: "class-builder builder", children: [ /* @__PURE__ */ jsx(PackageNameInput, {}), fields.length === 0 && /* @__PURE__ */ jsx( "div", { className: "builder-empty", "data-over": boolDataAttr(isOver), ref: setDroppableRef, children: "No fields." } ), fields.length > 0 && /* @__PURE__ */ jsxs("ul", { className: "builder-children", children: [ fields.map((field, i) => /* @__PURE__ */ jsx( SchematicBuilderContext.Provider, { value: { ...schematicBuilderContext, useSchematic: (selector) => useSchematic((schematic) => selector(schematic.children[i])), setSchematic: (toSet) => setSchematic((schematic) => ({ ...schematic, children: schematic.children.map( (f) => field === f ? { ...f, ...typeof toSet === "function" ? toSet(f) : toSet } : f ) })), removeSchematic: () => setSchematic((schematic) => ({ ...schematic, children: schematic.children.filter((f) => f !== field) })), parentPackage: joinPackages( schematicBuilderContext.parentPackage, packageSuffix ), parentId: id, ignoreDrop: draggedSchematic != null && draggedSchematic.id === fields[i - 1]?.id }, children: /* @__PURE__ */ jsx(ClassFieldBuilder, {}) }, field.id )), /* @__PURE__ */ jsx( "li", { className: "builder-child-droppable", "data-over": boolDataAttr(isOver && !shouldIgnoreDrop), ref: setDroppableRef } ) ] }), /* @__PURE__ */ jsx( "button", { className: "builder-input builder-new-child", type: "button", onClick: () => { setSchematic((schematic) => ({ ...schematic, children: [ ...schematic.children ?? [], createSchematic({ childName: "" }) ] })); }, ...preventDrag, children: "Add field" } ) ] }); } const CHILD_NAME_PLACEHOLDER$1 = "fieldName"; function ClassFieldBuilder() { const schematicBuilderContext = useSchematicBuilderContext(); const { schematicKinds, useSchematic, setSchematic, draggedSchematic, parentId, disableDrop, ignoreDrop } = schematicBuilderContext; const id = useSchematic((schematic) => schematic.id); const childName = useSchematic((schematic) => schematic.childName); const kind = useSchematic((schematic) => schematic.kind); const nullable = useSchematic((schematic) => schematic.nullable); const collapsed = useSchematic((schematic) => schematic.collapsed); const Builder = schematicKinds.get(kind)?.builder; const { attributes, listeners, setNodeRef: setDraggableRef, isDragging } = useDraggable({ id, data: { type: CLASS_DND_TYPE, node: /* @__PURE__ */ jsxs(Fragment, { children: [ [childName, kind].filter((s) => s).join(": "), nullable ? "?" : "" ] }) } }); const shouldIgnoreDrop = isDragging || ignoreDrop; const { setNodeRef: setDroppableRef, isOver } = useDroppable({ id, data: { parentId, sortBefore: id, ignored: shouldIgnoreDrop }, disabled: disableDrop || draggedSchematic?.type !== CLASS_DND_TYPE }); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "li", { className: "builder-child-droppable", "data-over": boolDataAttr(isOver && !shouldIgnoreDrop), ref: setDroppableRef } ), /* @__PURE__ */ jsxs( "li", { className: "builder-child class-builder-field", "data-dragging": boolDataAttr(isDragging), ...listeners, ...attributes, ref: setDraggableRef, "data-collapsible": boolDataAttr(Builder != null), children: [ /* @__PURE__ */ jsxs("div", { className: "builder-child-content", children: [ /* @__PURE__ */ jsx(ChildMarker, {}), /* @__PURE__ */ jsx( "input", { className: "builder-input class-builder-field-id", placeholder: CHILD_NAME_PLACEHOLDER$1, value: childName, onChange: (evt) => setSchematic({ childName: evt.target.value }), required: true, pattern: "[a-zA-Z_][a-zA-Z0-9_]*", style: { width: `${(childName || CHILD_NAME_PLACEHOLDER$1).length}ch` }, ...preventDrag } ), /* @__PURE__ */ jsx("span", { className: "builder-operator", children: "∶" }), /* @__PURE__ */ jsx( SchematicBuilderContext.Provider, { value: { ...schematicBuilderContext, parentChildName: childName }, children: /* @__PURE__ */ jsx(KindSelect, {}) } ), /* @__PURE__ */ jsx(NullableInput, {}), /* @__PURE__ */ jsx(ChildRemove, {}) ] }), Builder && !collapsed && /* @__PURE__ */ jsx( SchematicBuilderContext.Provider, { value: { ...schematicBuilderContext, parentChildName: childName, disableDrop: disableDrop || isDragging }, children: /* @__PURE__ */ jsx(Builder, {}) } ) ] } ) ] }); } const ENUM_DND_TYPE = "enum-entry"; function EnumBuilder() { const schematicBuilderContext = useSchematicBuilderContext(); const { useSchematic, setSchematic, draggedSchematic, disableDrop } = schematicBuilderContext; const id = useSchematic((schematic) => schematic.id); const fields = useSchematic((schematic) => schematic.children) ?? []; const shouldIgnoreDrop = draggedSchematic != null && draggedSchematic.id === fields.at(-1)?.id; const { setNodeRef: setDroppableRef, isOver } = useDroppable({ id: `${id}-append`, data: { parentId: id, sortBefore: null, ignored: shouldIgnoreDrop }, disabled: disableDrop || draggedSchematic?.type !== ENUM_DND_TYPE }); return /* @__PURE__ */ jsxs("div", { className: "enum-builder builder", children: [ /* @__PURE__ */ jsx(PackageNameInput, {}), fields.length === 0 && /* @__PURE__ */ jsx( "div", { className: "builder-empty", "data-over": boolDataAttr(isOver), ref: setDroppableRef, children: "No entries." } ), fields.length > 0 && /* @__PURE__ */ jsxs("ul", { className: "builder-children", children: [ fields.map((field, i) => /* @__PURE__ */ jsx( SchematicBuilderContext.Provider, { value: { ...schematicBuilderContext, useSchematic: (selector) => useSchematic((schematic) => selector(schematic.children[i])), setSchematic: (toSet) => setSchematic((schematic) => ({ ...schematic, children: schematic.children.map( (f) => field === f ? { ...f, ...typeof toSet === "function" ? toSet(f) : toSet } : f ) })), removeSchematic: () => setSchematic((schematic) => ({ ...schematic, children: schematic.children.filter((f) => f !== field) })), parentId: id, ignoreDrop: draggedSchematic != null && draggedSchematic.id === fields[i - 1]?.id }, children: /* @__PURE__ */ jsx(EnumEntryBuilder, {}) }, field.id )), /* @__PURE__ */ jsx( "li", { className: "builder-child-droppable", "data-over": boolDataAttr(isOver && !shouldIgnoreDrop), ref: setDroppableRef } ) ] }), /* @__PURE__ */ jsx( "button", { className: "builder-input builder-new-child", type: "button", onClick: () => { setSchematic((schematic) => ({ ...schematic, children: [ ...schematic.children ?? [], createSchematic({ childName: "" }) ] })); }, ...preventDrag, children: "Add entry" } ) ] }); } const CHILD_NAME_PLACEHOLDER = "ENTRY_NAME"; function EnumEntryBuilder() { const schematicBuilderContext = useSchematicBuilderContext(); const { schematicKinds, useSchematic, setSchematic, draggedSchematic, parentId, disableDrop, ignoreDrop } = schematicBuilderContext; const id = useSchematic((schematic) => schematic.id); const childName = useSchematic((schematic) => schematic.childName); const kind = useSchematic((schematic) => schematic.kind); const Builder = schematicKinds.get(kind)?.builder; const { attributes, listeners, setNodeRef: setDraggableRef, isDragging } = useDraggable({ id, data: { type: ENUM_DND_TYPE, node: /* @__PURE__ */ jsx(Fragment, { children: [childName, kind].filter((s) => s).join(": ") }) } }); const shouldIgnoreDrop = isDragging || ignoreDrop; const { setNodeRef: setDroppableRef, isOver } = useDroppable({ id, data: { parentId, sortBefore: id, ignored: shouldIgnoreDrop }, disabled: disableDrop || draggedSchematic?.type !== ENUM_DND_TYPE }); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "li", { className: "builder-child-droppable", "data-over": boolDataAttr(isOver && !shouldIgnoreDrop), ref: setDroppableRef } ), /* @__PURE__ */ jsx( "li", { className: "builder-child", "data-dragging": boolDataAttr(isDragging), ...listeners, ...attributes, ref: setDraggableRef, "data-collapsible": boolDataAttr(Builder != null), children: /* @__PURE__ */ jsxs("div", { className: "builder-child-content", children: [ /* @__PURE__ */ jsx(ChildMarker, {}), /* @__PURE__ */ jsx( "input", { className: "builder-input enum-builder-entry-id", placeholder: CHILD_NAME_PLACEHOLDER, value: childName, onChange: (evt) => setSchematic({ childName: evt.target.value }), required: true, pattern: "[a-zA-Z_][a-zA-Z0-9_]*", style: { width: `${(childName || CHILD_NAME_PLACEHOLDER).length}ch` }, ...preventDrag } ), /* @__PURE__ */ jsx(ChildRemove, {}) ] }) } ) ] }); } function ListableBuilder({ showKindSelect = true }) { const schematicBuilderContext = useSchematicBuilderContext(); const { useSchematic, setSchematic } = schematicBuilderContext; const id = useSchematic((schematic) => schematic.id); return /* @__PURE__ */ jsx("div", { className: "listable-builder builder", children: /* @__PURE__ */ jsx( SchematicBuilderContext.Provider, { value: { ...schematicBuilderContext, useSchematic: (selector) => useSchematic((schematic) => selector(schematic.children[0])), setSchematic: (toSet) => setSchematic((schematic) => ({ ...schematic, children: [ { ...schematic.children[0], ...typeof toSet === "function" ? toSet(schematic.children[0]) : toSet } ] })), removeSchematic: () => { }, parentId: id }, children: /* @__PURE__ */ jsx(ListableItemBuilder, { showKindSelect }) } ) }); } function ListableBuilderWithoutKindSelect() { return /* @__PURE__ */ jsx(ListableBuilder, { showKindSelect: false }); } function ListableItemBuilder({ showKindSelect = true }) { const { schematicKinds, useSchematic } = useSchematicBuilderContext(); const kind = useSchematic((schematic) => schematic.kind); const Builder = schematicKinds.get(kind)?.builder; return /* @__PURE__ */ jsxs(Fragment, { children: [ showKindSelect && /* @__PURE__ */ jsx(KindSelect, {}), Builder && /* @__PURE__ */ jsx(Builder, {}) ] }); } const LF = /\r?\n/; const EMPTY_LINE = /^\s*$/; const SPACE = /\s/; const LF_AFFIX = /(^\r?\n)|(\r?\n$)/g; function code(strings, ...values) { const minIndent = Math.min( ...strings.join("").split(LF).filter(isNotEmpty).map(indentWidth) ); let result = ""; for (let i = 0; i < values.length; ++i) { const trimmedString = trimNoninitialLines(strings[i], minIndent); result += trimmedString; result += indentNoninitialLines( String(values[i]), indentWidth(trimmedString.split(LF).at(-1)) ); } result += trimNoninitialLines(strings.at(-1), minIndent); return result.replace(LF_AFFIX, ""); } function isNotEmpty(line) { return !EMPTY_LINE.test(line); } function indentWidth(line) { let width = 0; for (const c of line) { if (!SPACE.test(c)) { break; } ++width; } return width; } function indentNoninitialLines(str, indentWidth2) { const lines = str.split(LF); const resultLines = [lines[0]]; for (let i = 1; i < lines.length; ++i) { const line = lines[i]; resultLines.push(line && " ".repeat(indentWidth2) + line); } return resultLines.join("\n"); } function trimNoninitialLines(str, indentWidth2) { const lines = str.split(LF); const resultLines = [lines[0]]; for (let i = 1; i < lines.length; ++i) { resultLines.push(lines[i].slice(indentWidth2)); } return resultLines.join("\n"); } const NULLABLE_SCHEMA = "io.kform.schemas.NullableSchema"; const anySchematicKind = { kind: "Any", package: "kotlin", name: "Any", schema: "io.kform.schemas.AnySchema", nullable: false, defaultValue: "null", scaffoldType: () => "Any?" }; const bigDecimalSchematicKind = { kind: "BigDecimal", package: "io.kform.datatypes", name: "BigDecimal", schema: "io.kform.schemas.BigDecimalSchema", defaultNullable: true, defaultValue: "BigDecimal.ZERO" }; const bigIntegerSchematicKind = { kind: "BigInteger", package: "io.kform.datatypes", name: "BigInteger", schema: "io.kform.schemas.BigIntegerSchema", defaultNullable: true, defaultValue: "BigInteger.ZERO" }; const booleanSchematicKind = { kind: "Boolean", package: "kotlin", name: "Boolean", schema: "io.kform.schemas.BooleanSchema", defaultValue: "false" }; const byteSchematicKind = { kind: "Byte", package: "kotlin", name: "Byte", schema: "io.kform.schemas.ByteSchema", defaultNullable: true, defaultValue: "0.toByte()" }; const charSchematicKind = { kind: "Char", package: "kotlin", name: "Char", schema: "io.kform.schemas.CharSchema", defaultNullable: true, defaultValue: "0.toChar()" }; const classSchematicKind = { kind: "Class", schema: "io.kform.schemas.ClassSchema", defaultValue: (schematic) => `${schematic.name}()`, builder: ClassBuilder, initChildren: () => [], defaultPackageSuffix: (childName) => childName.toLowerCase(), defaultName: (childName) => pascalCase(childName), scaffoldModel: (schematic, data) => code` data class ${schematic.name}( ${schematic.children?.map((childSchematic) => { const childData = { ...data, currentPath: joinPaths( data.currentPath, childSchematic.childName ) }; return annotate( scaffoldPropertyAnnotations(childSchematic, childData), `var ${childSchematic.childName}: ${scaffoldType( childSchematic, childData )} = ${scaffoldDefaultValue(childSchematic, childData)}` ); }).join(",\n")} ) `, scaffoldSchema: (schematic, data) => code` ClassSchema { ${schematic.children?.map( (childSchematic) => `${schematic.name}::${childSchematic.childName} { ${scaffoldSchema( childSchematic, { ...data, currentPath: joinPaths( data.currentPath, childSchematic.childName ) } )} }` ).join("\n")} } ` }; const doubleSchematicKind = { kind: "Double", package: "kotlin", name: "Double", schema: "io.kform.schemas.DoubleSchema", defaultNullable: true, defaultValue: "0.0" }; const enumSchematicKind = { kind: "Enum", schema: "io.kform.schemas.EnumSchema", defaultNullable: true, defaultValue: (schematic) => `${schematic.name}.${schematic.children?.[0].childName}`, builder: EnumBuilder, initChildren: () => [], defaultName: (childName) => pascalCase(childName), scaffoldModel: (schematic) => code` enum class ${schematic.name} { ${schematic.children?.map((childSchematic) => childSchematic.childName).join(",\n")} } ` }; const fileSchematicKind = { kind: "File", package: "io.kform.datatypes", name: "File", schema: "io.kform.schemas.FileSchema", defaultNullable: true, defaultValue: (_schematic, data) => { data.currentFile.imports.add("io.kform.datatypes.emptyPlaceholderFile"); return "emptyPlaceholderFile()"; }, propertyAnnotations: (_schematic, data) => data.useFileBase64Serializer ? "@Serializable(with = File.Base64Serializer::class)" : void 0 }; const floatSchematicKind = { kind: "Float", package: "kotlin", name: "Float", schema: "io.kform.schemas.FloatSchema", defaultNullable: true, defaultValue: "0f" }; const instantSchematicKind = { kind: "Instant", package: "kotlin.time", name: "Instant", schema: "io.kform.schemas.InstantSchema", defaultNullable: true, defaultValue: "Instant.fromEpochMilliseconds(0)", propertyAnnotations: (_schematic, data) => { data.currentFile.imports.add("kotlin.OptIn").add("kotlin.time.ExperimentalTime"); return "@OptIn(ExperimentalTime::class)"; } }; const intSchematicKind = { kind: "Int", package: "kotlin", name: "Int", schema: "io.kform.schemas.IntSchema", defaultNullable: true, defaultValue: "0" }; const listSchematicKind = { kind: "List", package: "kotlin.collections", name: "List", schema: "io.kform.schemas.ListSchema", defaultValue: "emptyList()", builder: ListableBuilder, initChildren: () => [createSchematic()], scaffoldType: (schematic, data) => `List<${scaffoldType(schematic.children[0], { ...data, currentPath: joinPaths(data.currentPath, "*") })}>`, scaffoldSchema: (schematic, data) => `ListSchema { ${scaffoldSchema(schematic.children[0], { ...data, currentPath: joinPaths(data.currentPath, "*") })} }` }; const localDateSchematicKind = { kind: "LocalDate", package: "kotlinx.datetime", name: "LocalDate", schema: "io.kform.schemas.LocalDateSchema", defaultNullable: true, defaultValue: "LocalDate(1970, 1, 1)" }; const localDateTimeSchematicKind = { kind: "LocalDateTime", package: "kotlinx.datetime", name: "LocalDateTime", schema: "io.kform.schemas.LocalDateTimeSchema", defaultNullable: true, defaultValue: "LocalDateTime(1970, 1, 1, 0, 0)" }; const longSchematicKind = { kind: "Long", package: "kotlin", name: "Long", schema: "io.kform.schemas.LongSchema", defaultNullable: true, defaultValue: "0L" }; const shortSchematicKind = { kind: "Short", package: "kotlin", name: "Short", schema: "io.kform.schemas.ShortSchema", defaultNullable: true, defaultValue: "0.toShort()" }; const stringSchematicKind = { kind: "String", package: "kotlin", name: "String", schema: "io.kform.schemas.StringSchema", defaultValue: '""' }; const tableSchematicKind = { kind: "Table", package: "io.kform.datatypes", name: "Table", schema: "io.kform.schemas.TableSchema", defaultValue: (_schematic, data) => { data.currentFile.imports.add("io.kform.datatypes.tableOf"); return "tableOf()"; }, builder: ListableBuilder, initChildren: () => [createSchematic()], propertyAnnotations: (_schematic, data) => data.useTableValuesSerializer ? "@Serializable(with = Table.ValuesSerializer::class)" : void 0, scaffoldType: (schematic, data) => `Table<${scaffoldType(schematic.children[0], { ...data, currentPath: joinPaths(data.currentPath, "*") })}>`, scaffoldSchema: (schematic, data) => `TableSchema { ${scaffoldSchema(schematic.children[0], { ...data, currentPath: joinPaths(data.currentPath, "*") })} }` }; const defaultSchematicKinds = [ anySchematicKind, bigDecimalSchematicKind, bigIntegerSchematicKind, booleanSchematicKind, byteSchematicKind, charSchematicKind, classSchematicKind, doubleSchematicKind, enumSchematicKind, fileSchematicKind, floatSchematicKind, instantSchematicKind, intSchematicKind, listSchematicKind, localDateSchematicKind, localDateTimeSchematicKind, longSchematicKind, shortSchematicKind, stringSchematicKind, tableSchematicKind ]; function scaffoldSchemas(schematic, data) { const fileName = joinPaths(data.currentDir, `${schematic.name}Schema.kt`); let file = data.files.get(fileName); if (!file) { data.files.set(fileName, file = ktFile(data)); data.files.set( joinPaths(data.currentDir, `${schematic.name}Validations.kt`), ktFile(data) ); } scaffoldSchemaDeclaration(schematic, { ...data, currentFile: file }); } function scaffoldSchemaDeclaration(schematic, data) { const schematicKind = data.schematicKinds.get(schematic.kind); if (!schematicKind) { throw new Error(`Unknown schematic kind ${schematic.kind}`); } const nDecls = data.currentFile.declarations.length; data.currentFile.declarations.push(""); const decl = `val ${schematic.name}Schema = ${schematicKind.scaffoldSchema?.(schematic, data) ?? `${simpleKtName(schematicKind.schema)}()`}`; data.currentFile.imports.add(schematicKind.schema); data.currentFile.declarations[nDecls] = data.currentPath === "/" ? jsExport(decl, data) : decl; } function scaffoldSchema(schematic, data) { const schematicKind = data.schematicKinds.get(schematic.kind); if (!schematicKind) { throw new Error(`Unknown schematic kind ${schematic.kind}`); } const schematicPackage = schematicKind.package ?? joinPackages(data.currentPackage, schematic.packageSuffix); let scaffoldedSchema; if (schematicKind.package == null && schematicPackage !== data.currentPackage) { scaffoldedSchema = `${schematic.name}Schema`; data.currentFile.imports.add(`${schematicPackage}.${scaffoldedSchema}`); scaffoldSchemas(schematic, { ...data, currentPackage: joinPackages( data.currentPackage, schematic.packageSuffix ), currentDir: joinPaths( data.currentDir, schematic.packageSuffix?.replace(".", "/") ) }); } else { scaffoldedSchema = schematicKind.scaffoldSchema?.(schematic, data) ?? `${simpleKtName(schematicKind.schema)}()`; data.currentFile.imports.add(schematicKind.schema); } if (schematic.nullable) { data.currentFile.imports.add(NULLABLE_SCHEMA); return `${simpleKtName(NULLABLE_SCHEMA)} { ${scaffoldedSchema} }`; } return scaffoldedSchema; } const serializerKt = "package <%= filePackage %>\n\nimport kotlin.js.JsExport\nimport kotlin.jvm.JvmOverloads\nimport kotlinx.serialization.json.Json\n\n/** Base JSON configuration. */\nprivate val JSON_CONFIG = Json.Default\n/** Pretty JSON configuration. */\nprivate val JSON_CONFIG_PRETTY = Json(JSON_CONFIG) { prettyPrint = true }\n\n/** JSON configuration. */\n@JvmOverloads\nfun jsonConfig(prettyPrint: Boolean = false): Json =\n if (prettyPrint) JSON_CONFIG_PRETTY else JSON_CONFIG\n\n@JsExport\n@JvmOverloads\nfun encode<%= formClass %>ToString(<%= formVar %>: <%= formClass %>, prettyPrint: Boolean = false): String =\n jsonConfig(prettyPrint).encodeToString(<%= formVar %>)\n\n@JsExport\nfun decode<%= formClass %>FromString(json: String): <%= formClass %> =\n jsonConfig().decodeFromString(json)\n"; function ejsTemplateFile(content, ejsData, { base64, binary, executable, ...ejsOptions } = {}) { return { content, ejsData, base64, binary, executable, ejsOptions, getContent: ejsTemplateFileContent }; } function ejsTemplateFileContent() { return render(this.content, this.ejsData, { strict: true, destructuredLocals: Object.keys(this.ejsData ?? {}), ...this.ejsOptions }); } function addEjsTemplateFile(data, fileName, content, ejsData, options) { data.files.set( joinPaths(data.currentDir, fileName), ejsTemplateFile(content, ejsData, options) ); } function scaffoldSerializer(schematic, data) { addEjsTemplateFile(data, `${schematic.name}Serializer.kt`, serializerKt, { filePackage: data.currentPackage, formVar: camelCase(schematic.name), formClass: schematic.name }); } const validatorKt = "package <%= filePackage %>\n\nimport io.kform.ExternalContexts\nimport io.kform.FormValidator\nimport io.kform.LocatedValidationError\nimport io.kform.LocatedValidationWarning\nimport io.kform.test.assertContainsMatchingIssue\nimport io.kform.test.assertNotContainsMatchingIssue\n\nval <%= formVar %>Validator by lazy { FormValidator(<%= formClass %>Schema) }\n\nfun validate<%= formClass %>(\n <%= formVar %>: <%= formClass %>,\n path: String,\n externalContexts: ExternalContexts = emptyMap(),\n) = <%= formVar %>Validator.validate(<%= formVar %>, path, externalContexts)\n\nsuspend fun assert<%= formClass %>ContainsError(\n path: String,\n code: String,\n <%= formVar %>: <%= formClass %>,\n externalContexts: ExternalContexts = emptyMap(),\n) =\n assertContainsMatchingIssue(\n LocatedValidationError(path, code),\n validate<%= formClass %>(<%= formVar %>, path, externalContexts),\n )\n\nsuspend fun assert<%= formClass %>NotContainsError(\n path: String,\n code: String,\n <%= formVar %>: <%= formClass %>,\n externalContexts: ExternalContexts = emptyMap(),\n) =\n assertNotContainsMatchingIssue(\n LocatedValidationError(path, code),\n validate<%= formClass %>(<%= formVar %>, path, externalContexts),\n )\n\nsuspend fun assert<%= formClass %>ContainsWarning(\n path: String,\n code: String,\n <%= formVar %>: <%= formClass %>,\n externalContexts: ExternalContexts = emptyMap(),\n) =\n assertContainsMatchingIssue(\n LocatedValidationWarning(path, code),\n validate<%= formClass %>(<%= formVar %>, path, externalContexts),\n )\n\nsuspend fun assert<%= formClass %>NotContainsWarning(\n path: String,\n code: String,\n <%= formVar %>: <%= formClass %>,\n externalContexts: ExternalContexts = emptyMap(),\n) =\n assertNotContainsMatchingIssue(\n LocatedValidationWarning(path, code),\n validate<%= formClass %>(<%= formVar %>, path, externalContexts),\n )\n"; function scaffoldValidator(schematic, data) { addEjsTemplateFile(data, `${schematic.name}Validator.kt`, validatorKt, { filePackage: data.currentPackage, formVar: camelCase(schematic.name), formClass: schematic.name }); } const useLayoutEffect = typeof document !== "undefined" ? React.useLayoutEffect : () => { }; function useMeasure(element, setMeasurement) { useLayoutEffect(() => { if (!element) { setMeasurement(void 0); return; } const cb = () => setMeasurement(element.getBoundingClientRect()); cb(); const observer = new ResizeObserver(([entry]) => entry && cb()); observer.observe(element); window.addEventListener("resize", cb); return () => { observer.disconnect(); window.removeEventListener("resize", cb); setMeasurement(void 0); }; }, [element, setMeasurement]); } function SchematicBuilderConfig({ children }) { const [anchorEl, setAnchorEl] = React.useState( null ); const [popoverEl, setPopoverEl] = React.useState(null); useMeasure(anchorEl, (measurement) => { if (popoverEl && measurement) { popoverEl.style.setProperty("--anchor-top", `${measurement.top}px`); popoverEl.style.setProperty("--anchor-left", `${measurement.left}px`); popoverEl.style.setProperty("--anchor-bottom", `${measurement.bottom}px`); popoverEl.style.setProperty("--anchor-right", `${measurement.right}px`); popoverEl.style.setProperty("--anchor-width", `${measurement.width}px`); popoverEl.style.setProperty("--anchor-height", `${measurement.height}px`); } }); useMeasure(popoverEl, (measurement) => { if (popoverEl && measurement) { popoverEl.style.setProperty("--width", `${measurement.width}px`); popoverEl.style.setProperty("--height", `${measurement.height}px`); } }); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "button", { type: "button", className: "builder-icon-button builder-action builder-config-schematic", popoverTarget: "builder-config-popover", title: "Config", "aria-label": "Config", ref: setAnchorEl, children: "⚙️" } ), /* @__PURE__ */ jsx("div", { popover: "", id: "builder-config-popover", ref: setPopoverEl, children }) ] }); } const kotlinMainData = { currentDir: "shared/src/commonMain/kotlin" }; const kotlinTestData = { currentDir: "shared/src/commonTest/kotlin" }; function SchematicBuilder({ name = "kform", title = document.title, basePath, basePackage, baseDir, defaultRootClassPackage = "org.example", defaultRootClassName = "MyForm", schematicKinds = defaultSchematicKinds, scaffolders = [ configScaffolder(scaffoldSchemas, kotlinMainData), configScaffolder(scaffoldModels, kotlinMainData), configScaffolder(scaffoldSerializer, kotlinMainData), configScaffolder(scaffoldValidator, kotlinTestData) ], scaffoldingData = (s