diff --git a/artifacts/hero-ui-renderer/.gitignore b/artifacts/hero-ui-renderer/.gitignore new file mode 100644 index 00000000..8f322f0d --- /dev/null +++ b/artifacts/hero-ui-renderer/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/artifacts/hero-ui-renderer/.npmrc b/artifacts/hero-ui-renderer/.npmrc new file mode 100644 index 00000000..f819c90f --- /dev/null +++ b/artifacts/hero-ui-renderer/.npmrc @@ -0,0 +1,2 @@ +public-hoist-pattern[]=*@heroui/* +package-lock=true \ No newline at end of file diff --git a/artifacts/hero-ui-renderer/.vscode/settings.json b/artifacts/hero-ui-renderer/.vscode/settings.json new file mode 100644 index 00000000..3662b370 --- /dev/null +++ b/artifacts/hero-ui-renderer/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/artifacts/hero-ui-renderer/LICENSE b/artifacts/hero-ui-renderer/LICENSE new file mode 100644 index 00000000..7f91f848 --- /dev/null +++ b/artifacts/hero-ui-renderer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Next UI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/artifacts/hero-ui-renderer/README.md b/artifacts/hero-ui-renderer/README.md new file mode 100644 index 00000000..36df74da --- /dev/null +++ b/artifacts/hero-ui-renderer/README.md @@ -0,0 +1,53 @@ +# Next.js & HeroUI Template + +This is a template for creating applications using Next.js 14 (app directory) and HeroUI (v2). + +[Try it on CodeSandbox](https://githubbox.com/heroui-inc/heroui/next-app-template) + +## Technologies Used + +- [Next.js 14](https://nextjs.org/docs/getting-started) +- [HeroUI v2](https://heroui.com/) +- [Tailwind CSS](https://tailwindcss.com/) +- [Tailwind Variants](https://tailwind-variants.org) +- [TypeScript](https://www.typescriptlang.org/) +- [Framer Motion](https://www.framer.com/motion/) +- [next-themes](https://github.com/pacocoursey/next-themes) + +## How to Use + +### Use the template with create-next-app + +To create a new project based on this template using `create-next-app`, run the following command: + +```bash +npx create-next-app -e https://github.com/heroui-inc/next-app-template +``` + +### Install dependencies + +You can use one of them `npm`, `yarn`, `pnpm`, `bun`, Example using `npm`: + +```bash +npm install +``` + +### Run the development server + +```bash +npm run dev +``` + +### Setup pnpm (optional) + +If you are using `pnpm`, you need to add the following code to your `.npmrc` file: + +```bash +public-hoist-pattern[]=*@heroui/* +``` + +After modifying the `.npmrc` file, you need to run `pnpm install` again to ensure that the dependencies are installed correctly. + +## License + +Licensed under the [MIT license](https://github.com/heroui-inc/next-app-template/blob/main/LICENSE). diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/ErrorBoundary.tsx b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000..5b59fdb1 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,90 @@ +import { Component, ErrorInfo } from "react" +import { ErrorDisplay } from "../ErrorDisplay" +import { ErrorBoundaryProps, ErrorBoundaryState } from "./interface" + +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { + hasError: false, + errorMessage: null, + } + } + + static getDerivedStateFromError(): ErrorBoundaryState { + return { hasError: true } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // 增强错误信息 - 检测未定义组件错误 + const enhancedErrorMessage = this.processErrorMessage(error, errorInfo) + + this.setState({ errorMessage: enhancedErrorMessage, errorInfo }) + this.props.onError(enhancedErrorMessage) + } + + // 处理错误信息,提供更有意义的反馈 + processErrorMessage(error: Error, errorInfo: ErrorInfo): string { + // 检测React典型的未定义组件错误 + const undefinedComponentMatch = error.message.match( + /type is invalid.*?but got: undefined.*?You likely forgot to export/i, + ) + + if (undefinedComponentMatch) { + // 从错误堆栈中提取组件名称 + let componentName = "Unknown" + let importSource = "unknown module" + + // 尝试从错误堆栈中获取有用的信息 + if (errorInfo.componentStack) { + // 提取组件名称 - 通常是第一行包含的信息 + const componentMatch = errorInfo.componentStack.match( + /\s+at\s+([A-Za-z0-9_]+)/, + ) + if (componentMatch && componentMatch[1]) { + componentName = componentMatch[1] + } + + // 尝试提取可能的导入源 + const lines = error.stack?.split("\n") || [] + for (const line of lines) { + const moduleMatch = line.match(/from ['"]([^'"]+)['"]/) + if (moduleMatch) { + importSource = moduleMatch[1] + break + } + } + } + + return `Component error: The component "${componentName}" being rendered is undefined. This likely happens when: +1. You're importing a component that doesn't exist in "${importSource}" +2. You've misspelled the component name during import or usage +3. The component exists but wasn't properly exported + +Check your imports and component usage.` + } + + return error.message + } + + componentDidUpdate(prevProps: ErrorBoundaryProps) { + // Reset error state when files prop changes to allow retry + if (this.state.hasError && this.props.files !== prevProps.files) { + this.setState({ + hasError: false, + errorMessage: null, + errorInfo: null, + }) + } + } + + render() { + if (this.state.hasError) { + return + } + + return this.props.children + } +} + +export default ErrorBoundary diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/index.ts b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/index.ts new file mode 100644 index 00000000..e0bddd2b --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { default as ErrorBoundary } from "./ErrorBoundary"; diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/interface.ts b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/interface.ts new file mode 100644 index 00000000..8b0b72ec --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorBoundary/interface.ts @@ -0,0 +1,15 @@ +import { ErrorInfo } from "react" + +import { ReactNode } from "react" + +export interface ErrorBoundaryProps { + children: ReactNode + onError: (errorMessage: string) => void + files?: Record +} + +export interface ErrorBoundaryState { + hasError: boolean + errorMessage?: string | null + errorInfo?: ErrorInfo | null +} diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/ErrorDisplay.tsx b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/ErrorDisplay.tsx new file mode 100644 index 00000000..2f366cb1 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/ErrorDisplay.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from "react"; +import type { ErrorDisplayProps } from "./interface"; + +const ErrorDisplay: React.FC = ({ errorMessage }) => { + const [visible, setVisible] = useState(false); + + useEffect(() => { + setTimeout(() => setVisible(true), 100); + }, []); + + return ( +
+
+
+
+
+
+ +

+ {">"} SYSTEM ERROR DETECTED +

+ +
+
+ {">"} Error Stack Trace: +
+
+          {errorMessage?.split("\n").map((line, index) => (
+            
+ {line} +
+ ))} +
+
+ +
+ * Please try clicking " + EXECUTE AUTO FIX + " in the bottom right corner to fix the issue. If you think this is + a bug, please{" "} + + report it in Compoder's GitHub issues + +
+
+ ); +}; + +export default ErrorDisplay; diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/index.ts b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/index.ts new file mode 100644 index 00000000..2a860a68 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/index.ts @@ -0,0 +1,2 @@ +export { default as ErrorDisplay } from './ErrorDisplay'; +export type { ErrorDisplayProps } from './interface'; diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/interface.ts b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/interface.ts new file mode 100644 index 00000000..08e1fa26 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ErrorDisplay/interface.ts @@ -0,0 +1,3 @@ +export interface ErrorDisplayProps { + errorMessage?: string | null; +} diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ReactCodeRenderer.tsx b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ReactCodeRenderer.tsx new file mode 100644 index 00000000..8fd737d1 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/ReactCodeRenderer.tsx @@ -0,0 +1,276 @@ +import React, { useEffect, useState } from "react"; +import { transform } from "@babel/standalone"; +import path from "path-browserify"; +import { ErrorDisplay } from "./ErrorDisplay"; +import { ErrorBoundary } from "./ErrorBoundary"; +import { + DynamicComponentRendererProps, + ModuleCache, + ExportsObject, +} from "./interface"; + +// 添加全局类型声明 +declare global { + interface Window { + _moduleImportMap: { + [importPath: string]: { + importingFile: string; + importedModule: string; + }; + }; + } +} + +const DynamicComponentRenderer: React.FC = ({ + files, + entryFile, + customRequire, + onError, + onSuccess, +}) => { + const [Component, setComponent] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const modules: ModuleCache = {}; + + const processFile = (filename: string): any => { + if (modules[filename]) { + return modules[filename].exports; + } + const code = files[filename]; + if (!code) { + throw new Error(`File not found: ${filename}`); + } + + const transformedCode = transform(code, { + filename, + presets: ["react", "env", "typescript"], + plugins: ["transform-modules-commonjs"], + }).code; + + const exports: ExportsObject = {}; + const myModule = { exports }; + + const ComponentModule = new Function( + "require", + "module", + "exports", + "__filename", + "React", + transformedCode! + ); + + ComponentModule( + (importPath: string) => { + const resolvedPath = importPath.startsWith(".") + ? path.join(path.dirname(filename), importPath).replace(/^\//, "") + : importPath; + + const possiblePaths = [ + resolvedPath, + resolvedPath + ".ts", + resolvedPath + ".tsx", + resolvedPath + "/index.ts", + resolvedPath + "/index.tsx", + resolvedPath.replace(/\.(ts|tsx)$/, ""), + ]; + + const normalizedPath = Object.keys(files).find((file) => + possiblePaths.includes(file) + ); + + if (normalizedPath) { + try { + const result = processFile(normalizedPath); + + if ( + result === undefined || + (typeof result === "object" && + Object.keys(result).length === 0 && + !result.default) + ) { + throw new Error( + `Module "${importPath}" imported in "${filename}" doesn't have any exports. Make sure you've correctly exported components/functions from this module.` + ); + } + + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error( + `Error while processing import "${importPath}" in "${filename}": ${error.message}` + ); + } + throw error; + } + } + + // 处理外部依赖库导入 + try { + const externalModule = customRequire(importPath); + + // 记录从哪个文件导入了哪个模块,用于错误分析 + const importRecord = { + importingFile: filename, + importedModule: importPath, + }; + + // 存储该导入记录 + if (!window._moduleImportMap) { + window._moduleImportMap = {}; + } + window._moduleImportMap[importPath] = importRecord; + + // 增强的Proxy,处理多种情况 + return new Proxy(externalModule, { + get: (target, prop) => { + // 排除Symbol和私有属性 + if ( + typeof prop === "symbol" || + prop.toString().startsWith("_") + ) { + return target[prop]; + } + + // 检查属性是否存在于目标对象中 + if (prop in target) { + const value = target[prop]; + + // 检查值是否为undefined (即使属性存在但值为undefined) + if (value === undefined && prop !== "default") { + throw new Error( + `Component "${String( + prop + )}" exists in module "${importPath}" but its value is undefined. This may indicate a packaging or export issue with the module.` + ); + } + + return value; + } + + // 特殊处理: 某些库可能有不同的导出方式,尝试寻找近似的组件名 + const keys = Object.keys(target); + const similarNames = keys.filter( + (k) => + k.toLowerCase() === String(prop).toLowerCase() || + k.replace(/[_-]/g, "") === String(prop).replace(/[_-]/g, "") + ); + + if (similarNames.length > 0) { + const suggestions = similarNames.join(", "); + throw new Error( + `Component "${String( + prop + )}" does not exist in module "${importPath}". Did you mean: ${suggestions}?` + ); + } + + // 当访问不存在的属性时,抛出更明确的错误 + throw new Error( + `Component "${String( + prop + )}" does not exist in module "${importPath}". Available components are: ${Object.keys( + target + ).join(", ")}.` + ); + }, + }); + } catch (error) { + // 处理外部库加载错误 + if (error instanceof Error) { + // 如果错误信息已经是我们的自定义错误,直接传递 + if (error.message.includes("does not exist in module")) { + throw error; + } + throw new Error( + `Error loading external module "${importPath}": ${error.message}` + ); + } + throw error; + } + }, + myModule, + exports, + filename, + require("react") + ); + + modules[filename] = myModule; + return myModule.exports; + }; + + const parseComponents = async () => { + try { + setError(null); + processFile(entryFile); + + const exportedComponent = modules[entryFile].exports.default; + if (!exportedComponent) { + const errorMsg = `Component not found: The default export from "${entryFile}" is undefined. Please check if you've correctly exported your component with "export default YourComponent".`; + setError(errorMsg); + onError(errorMsg); + return; + } + + setComponent(() => exportedComponent); + onSuccess(); + } catch (error: any) { + console.error("parseComponents error:", error); + + // 增强错误信息处理 - 检测未定义组件的使用 + const undefinedComponentMatch = error.message.match( + /type is invalid.*?but got: undefined.*?You likely forgot to export/i + ); + const missingComponentMatch = error.message.match( + /Component "([^"]+)" does not exist in module "([^"]+)"/ + ); + + if (missingComponentMatch) { + // 直接使用我们的自定义错误信息 + setError(error.message); + onError(error.message); + } else if (undefinedComponentMatch) { + // 尝试从错误堆栈中提取组件名称 + const componentNameMatch = error.stack?.match( + /at ([A-Za-z0-9_]+) \(/ + ); + const componentName = componentNameMatch + ? componentNameMatch[1] + : "Unknown"; + + // 提取可能的导入源 + const importSourceMatch = error.stack?.match(/from ['"]([^'"]+)['"]/); + const importSource = importSourceMatch + ? importSourceMatch[1] + : "a module"; + + const enhancedErrorMsg = `Missing component error: The component "${componentName}" being rendered is undefined. This often happens when you import a non-existent component (e.g., from ${importSource}). Please check your imports and make sure all components exist in their respective packages.`; + setError(enhancedErrorMsg); + onError(enhancedErrorMsg); + } else { + setError("parse component error: " + error.message); + onError("parse component error: " + error.message); + } + } + }; + + parseComponents(); + }, [files, entryFile, customRequire, onError]); + + if (error) { + return ; + } + + if (!Component) { + return null; + } + + return ( + + + + ); +}; + +export default DynamicComponentRenderer; diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/index.ts b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/index.ts new file mode 100644 index 00000000..805c4853 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/index.ts @@ -0,0 +1,2 @@ +export { default as ReactCodeRenderer } from "./ReactCodeRenderer" +export type { DynamicComponentRendererProps } from "./interface" diff --git a/artifacts/hero-ui-renderer/app/ReactCodeRenderer/interface.ts b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/interface.ts new file mode 100644 index 00000000..0b354d9d --- /dev/null +++ b/artifacts/hero-ui-renderer/app/ReactCodeRenderer/interface.ts @@ -0,0 +1,17 @@ +export interface DynamicComponentRendererProps { + files: { [key: string]: string }; + entryFile: string; + customRequire: (importPath: string) => any; + onError: (errorMessage: string) => void; + onSuccess: () => void; +} + +export interface ModuleCache { + [key: string]: { + exports: any; + }; +} + +export interface ExportsObject { + [key: string]: any; +} diff --git a/artifacts/hero-ui-renderer/app/customRequire.ts b/artifacts/hero-ui-renderer/app/customRequire.ts new file mode 100644 index 00000000..c5231412 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/customRequire.ts @@ -0,0 +1,20 @@ +export const customRequire = (moduleName: string) => { + const modules: { [key: string]: any } = { + // base modules + react: require("react"), + "react-dom": require("react-dom"), + "lucide-react": require("lucide-react"), + "next/link": require("next/link"), + "next/image": require("next/image"), + "framer-motion": require("framer-motion"), + "react-hook-form": require("react-hook-form"), + recharts: require("recharts"), + "@heroui/react": require("@heroui/react"), + }; + + if (modules[moduleName]) { + return modules[moduleName]; + } + + throw new Error(`Module ${moduleName} not found`); +}; diff --git a/artifacts/hero-ui-renderer/app/error.tsx b/artifacts/hero-ui-renderer/app/error.tsx new file mode 100644 index 00000000..9ed5104e --- /dev/null +++ b/artifacts/hero-ui-renderer/app/error.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useEffect } from "react"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + /* eslint-disable no-console */ + console.error(error); + }, [error]); + + return ( +
+

Something went wrong!

+ +
+ ); +} diff --git a/artifacts/hero-ui-renderer/app/globals.css b/artifacts/hero-ui-renderer/app/globals.css new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/artifacts/hero-ui-renderer/app/layout.tsx b/artifacts/hero-ui-renderer/app/layout.tsx new file mode 100644 index 00000000..4904e854 --- /dev/null +++ b/artifacts/hero-ui-renderer/app/layout.tsx @@ -0,0 +1,275 @@ +import "@/app/globals.css"; +import { Metadata, Viewport } from "next"; +import clsx from "clsx"; + +import { Providers } from "./providers"; + +export const metadata: Metadata = { + title: "HeroUI(NextUI) Renderer", + description: "HeroUI(NextUI) Renderer", +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "white" }, + { media: "(prefers-color-scheme: dark)", color: "black" }, + ], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + {children} + + + {/* Tailwind CDN */} + + + {/* Tailwind CDN Configuration */} +