diff --git a/core/morph/api/app.py b/core/morph/api/app.py
index 9ead037..9f4fe99 100644
--- a/core/morph/api/app.py
+++ b/core/morph/api/app.py
@@ -21,9 +21,6 @@
inertia_request_validation_exception_handler,
inertia_version_conflict_exception_handler,
)
-from starlette.middleware.cors import CORSMiddleware
-from starlette.middleware.sessions import SessionMiddleware
-
from morph.api.error import ApiBaseError, InternalError, render_error_html
from morph.api.handler import router
from morph.api.plugin import plugin_app
@@ -32,6 +29,8 @@
MorphFunctionMetaObjectCacheManager,
MorphGlobalContext,
)
+from starlette.middleware.cors import CORSMiddleware
+from starlette.middleware.sessions import SessionMiddleware
# configuration values
@@ -42,10 +41,11 @@
# set true to MORPH_LOCAL_DEV_MODE to use local frontend server
is_local_dev_mode = True if os.getenv("MORPH_LOCAL_DEV_MODE") == "true" else False
+project_root = find_project_root_dir()
+
def custom_compile_logic():
logger.info("Compiling python and sql files...")
- project_root = find_project_root_dir()
context = MorphGlobalContext.get_instance()
errors = context.load(project_root)
if len(errors) > 0:
@@ -129,8 +129,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
inertia_request_validation_exception_handler,
)
-frontend_dir = os.path.join(os.getcwd(), ".morph", "frontend")
-
def get_inertia_config():
templates_dir = os.path.join(Path(__file__).resolve().parent, "templates")
@@ -148,15 +146,16 @@ def get_inertia_config():
use_flash_messages=True,
use_flash_errors=True,
entrypoint_filename="main.tsx",
- assets_prefix="/src",
+ root_directory=".morph/frontend",
dev_url=frontend_url,
)
return InertiaConfig(
templates=Jinja2Templates(directory=templates_dir),
- manifest_json_path=os.path.join(frontend_dir, "dist", "manifest.json"),
+ manifest_json_path=os.path.join(project_root, "dist", "manifest.json"),
environment="production",
entrypoint_filename="main.tsx",
+ root_directory=".morph/frontend",
)
@@ -167,13 +166,13 @@ def get_inertia_config():
if is_local_dev_mode:
app.mount(
"/src",
- StaticFiles(directory=os.path.join(frontend_dir, "src")),
+ StaticFiles(directory=os.path.join(project_root, "src")),
name="src",
)
else:
app.mount(
"/assets",
- StaticFiles(directory=os.path.join(frontend_dir, "dist", "assets")),
+ StaticFiles(directory=os.path.join(project_root, "dist", "assets")),
name="assets",
)
diff --git a/core/morph/api/cloud/client.py b/core/morph/api/cloud/client.py
index 8d60a34..d0d197c 100644
--- a/core/morph/api/cloud/client.py
+++ b/core/morph/api/cloud/client.py
@@ -132,14 +132,20 @@ def verify_api_secret(self) -> MorphClientResponse:
@validate_project_id
def initiate_deployment(
- self, project_id: str, image_build_log: str, image_checksum: str
+ self,
+ project_id: str,
+ image_build_log: str,
+ image_checksum: str,
+ config: Optional[dict[str, Any]] = None,
) -> MorphClientResponse:
path = "deployment"
- body = {
+ body: dict[str, Any] = {
"projectId": project_id,
"imageBuildLog": image_build_log,
"imageChecksum": image_checksum,
}
+ if config:
+ body["config"] = config
return self.request(method="POST", path=path, data=body)
diff --git a/core/morph/api/service.py b/core/morph/api/service.py
index 744aeea..d5a350e 100644
--- a/core/morph/api/service.py
+++ b/core/morph/api/service.py
@@ -7,7 +7,6 @@
import time
import uuid
from contextlib import redirect_stdout
-from pathlib import Path
from typing import Any
import click
@@ -284,11 +283,11 @@ async def file_upload_service(input: UploadFileService) -> Any:
)
# Read the saved file path from the cache (always created as following path)
- cache_file = Path(find_project_root_dir()).joinpath(
- ".morph/cache/file_upload.md"
- )
- with open(cache_file, "r") as f:
- saved_filepath = f.read()
+ saved_filepath = ""
+ cache_file = "/tmp/file_upload.cache"
+ if os.path.exists(cache_file):
+ with open(cache_file, "r") as f:
+ saved_filepath = f.read()
# Remove the temporary directory
if os.path.exists(temp_dir):
diff --git a/core/morph/api/templates/index.html b/core/morph/api/templates/index.html
index 9132d42..8265186 100644
--- a/core/morph/api/templates/index.html
+++ b/core/morph/api/templates/index.html
@@ -16,13 +16,6 @@
window.__vite_plugin_react_preamble_installed__ = true;
{% endif %}
-
-
-
{% inertia_body %}
diff --git a/core/morph/config/project.py b/core/morph/config/project.py
index 9c42bc5..7e6b914 100644
--- a/core/morph/config/project.py
+++ b/core/morph/config/project.py
@@ -1,5 +1,5 @@
import os
-from typing import List, Optional
+from typing import Dict, List, Optional
import yaml
from pydantic import BaseModel, Field
@@ -13,6 +13,20 @@
from morph.task.utils.morph import find_project_root_dir
+class BuildConfig(BaseModel):
+ runtime: Optional[str] = None
+ framework: Optional[str] = "morph"
+ package_manager: Optional[str] = None
+ context: Optional[str] = None
+ build_args: Optional[Dict[str, str]] = None
+
+
+class DeploymentConfig(BaseModel):
+ provider: Optional[str] = "aws"
+ aws: Optional[Dict[str, Optional[str]]] = None
+ gcp: Optional[Dict[str, Optional[str]]] = None
+
+
class MorphProject(BaseModel):
profile: Optional[str] = "default"
source_paths: List[str] = Field(default_factory=lambda: ["src"])
@@ -21,6 +35,8 @@ class MorphProject(BaseModel):
package_manager: str = Field(
default="pip", description="Package manager to use, e.g., pip or poetry."
)
+ build: Optional[BuildConfig] = Field(default_factory=BuildConfig)
+ deployment: Optional[DeploymentConfig] = Field(default_factory=DeploymentConfig)
class Config:
arbitrary_types_allowed = True
@@ -72,9 +88,115 @@ def save_project(project_root: str, project: MorphProject) -> None:
old_config_path = os.path.join(project_root, "morph_project.yaml")
if os.path.exists(old_config_path):
with open(old_config_path, "w") as f:
- yaml.safe_dump(project.model_dump(), f)
+ f.write(dump_project_yaml(project))
return
config_path = os.path.join(project_root, "morph_project.yml")
with open(config_path, "w") as f:
- yaml.safe_dump(project.model_dump(), f)
+ f.write(dump_project_yaml(project))
+
+
+def dump_project_yaml(project: MorphProject) -> str:
+ source_paths = "\n- ".join([""] + project.source_paths)
+
+ # Default values
+ build_runtime = ""
+ build_framework = ""
+ build_package_manager = ""
+ build_context = "."
+ build_args_str = "\n # - ARG_NAME=value\n # - ANOTHER_ARG=value"
+ deployment_provider = "aws"
+ deployment_aws_region = "us-east-1"
+ deployment_aws_memory = "1024"
+ deployment_aws_timeout = "300"
+ deployment_aws_concurrency = "1"
+ deployment_gcp_region = "us-central1"
+ deployment_gcp_memory = "1Gi"
+ deployment_gcp_cpu = "1"
+ deployment_gcp_concurrency = "80"
+ deployment_gcp_timeout = "300"
+
+ # Set values if build exists
+ if project.build:
+ if project.build.runtime:
+ build_runtime = project.build.runtime or ""
+ if project.build.framework:
+ build_framework = project.build.framework or ""
+ if project.build.package_manager:
+ build_package_manager = project.build.package_manager or ""
+ if project.build.context:
+ build_context = f"{project.build.context}" or "."
+ if project.build.build_args:
+ build_args_items = []
+ for key, value in project.build.build_args.items():
+ build_args_items.append(f"{key}={value}")
+ build_args_str = (
+ "\n # - ".join([""] + build_args_items)
+ if build_args_items
+ else "\n # - ARG_NAME=value\n # - ANOTHER_ARG=value"
+ )
+
+ # Set values if deployment exists
+ if project.deployment:
+ if project.deployment.provider:
+ deployment_provider = project.deployment.provider or "aws"
+ if project.deployment.aws:
+ deployment_aws_region = project.deployment.aws.get("region") or "us-east-1"
+ deployment_aws_memory = project.deployment.aws.get("memory") or "1024"
+ deployment_aws_timeout = project.deployment.aws.get("timeout") or "300"
+ deployment_aws_concurrency = (
+ project.deployment.aws.get("concurrency") or "1"
+ )
+ if project.deployment.gcp:
+ deployment_gcp_region = (
+ project.deployment.gcp.get("region") or "us-central1"
+ )
+ deployment_gcp_memory = project.deployment.gcp.get("memory") or "1Gi"
+ deployment_gcp_cpu = project.deployment.gcp.get("cpu") or "1"
+ deployment_gcp_concurrency = (
+ project.deployment.gcp.get("concurrency") or "80"
+ )
+ deployment_gcp_timeout = project.deployment.gcp.get("timeout") or "300"
+ else:
+ # Use default DeploymentConfig
+ deployment_provider = "aws"
+
+ return f"""
+version: '1'
+
+# Framework Settings
+default_connection: {project.default_connection}
+source_paths:{source_paths}
+
+# Cloud Settings
+# profile: {project.profile} # Defined in the Profile Section in `~/.morph/credentials`
+# project_id: {project.project_id or "null"}
+
+# Build Settings
+build:
+ # These settings are required when there is no Dockerfile in the project root.
+ # They define the environment in which the project will be built
+ runtime: {build_runtime} # python3.9, python3.10, python3.11, python3.12
+ framework: {build_framework}
+ package_manager: {build_package_manager} # pip, poetry, uv
+ # These settings are required when there is a Dockerfile in the project root.
+ # They define how the Docker image will be built
+ # context: {build_context}
+ # build_args:{build_args_str}
+
+# Deployment Settings
+deployment:
+ provider: {deployment_provider} # aws or gcp (default is aws)
+ # These settings are used only when you want to customize the deployment settings
+ # aws:
+ # region: {deployment_aws_region}
+ # memory: {deployment_aws_memory}
+ # timeout: {deployment_aws_timeout}
+ # concurrency: {deployment_aws_concurrency}
+ # gcp:
+ # region: {deployment_gcp_region}
+ # memory: {deployment_gcp_memory}
+ # cpu: {deployment_gcp_cpu}
+ # concurrency: {deployment_gcp_concurrency}
+ # timeout: {deployment_gcp_timeout}
+"""
diff --git a/core/morph/frontend/template/.eslintrc.cjs b/core/morph/frontend/template/.eslintrc.cjs
deleted file mode 100644
index d6c9537..0000000
--- a/core/morph/frontend/template/.eslintrc.cjs
+++ /dev/null
@@ -1,18 +0,0 @@
-module.exports = {
- root: true,
- env: { browser: true, es2020: true },
- extends: [
- 'eslint:recommended',
- 'plugin:@typescript-eslint/recommended',
- 'plugin:react-hooks/recommended',
- ],
- ignorePatterns: ['dist', '.eslintrc.cjs'],
- parser: '@typescript-eslint/parser',
- plugins: ['react-refresh'],
- rules: {
- 'react-refresh/only-export-components': [
- 'warn',
- { allowConstantExport: true },
- ],
- },
-}
diff --git a/core/morph/frontend/template/.gitignore b/core/morph/frontend/template/.gitignore
deleted file mode 100644
index 1e47915..0000000
--- a/core/morph/frontend/template/.gitignore
+++ /dev/null
@@ -1,32 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
-
-# development
-local-test
-
-# project files
-src/pages/*
-!src/pages/.gitkeep
-constants.js
diff --git a/core/morph/frontend/template/index.html b/core/morph/frontend/template/index.html
deleted file mode 100644
index 8467e8f..0000000
--- a/core/morph/frontend/template/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- Morph Project
-
-
-
-
-
-
diff --git a/core/morph/frontend/template/package.json b/core/morph/frontend/template/package.json
deleted file mode 100644
index cf5af0e..0000000
--- a/core/morph/frontend/template/package.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "name": "morph-frontend",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite --port",
- "build": "tsc -b && vite build",
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
- },
- "dependencies": {
- "@dagrejs/dagre": "^1.1.4",
- "@xyflow/react": "^12.4.1",
- "tailwind-variants": "^0.3.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1"
- },
- "devDependencies": {
- "@inertiajs/react": "^1.2.0",
- "@mdx-js/rollup": "^3.1.0",
- "@stefanprobst/rehype-extract-toc": "^2.2.1",
- "@types/mdx": "^2.0.13",
- "@types/node": "^20.14.11",
- "@types/react": "^18.3.3",
- "@types/react-dom": "^18.3.0",
- "@typescript-eslint/eslint-plugin": "^7.15.0",
- "@typescript-eslint/parser": "^7.15.0",
- "@morph-data/components": "0.2.0-beta.2",
- "@vitejs/plugin-react-swc": "^3.5.0",
- "class-variance-authority": "^0.7.1",
- "eslint": "^8.57.0",
- "eslint-plugin-react-hooks": "^4.6.2",
- "eslint-plugin-react-refresh": "^0.4.7",
- "react-error-boundary": "^4.1.2",
- "rehype-slug": "^6.0.0",
- "remark-gfm": "^4.0.0",
- "typescript": "^5.2.2",
- "vite": "^5.3.4",
- "vite-plugin-restart": "^0.4.2"
- }
-}
diff --git a/core/morph/frontend/template/src/admin/AdminPage.tsx b/core/morph/frontend/template/src/admin/AdminPage.tsx
deleted file mode 100644
index f42580a..0000000
--- a/core/morph/frontend/template/src/admin/AdminPage.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { DataPipeline } from "./datapipeline/DataPipeline";
-
-export const AdminPage = () => {
- return ;
-};
diff --git a/core/morph/frontend/template/src/admin/common/useResourcesQuery.ts b/core/morph/frontend/template/src/admin/common/useResourcesQuery.ts
deleted file mode 100644
index f1581c8..0000000
--- a/core/morph/frontend/template/src/admin/common/useResourcesQuery.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useQuery } from "./utils/useQuery";
-
-type Resource = {
- alias: string;
- path: string;
- connection?: string | null;
- output_paths: string[];
- public?: boolean | null;
- output_type?: string | null;
- data_requirements?: string[] | null;
-};
-
-type GetResourcesResponse = {
- resources: Resource[];
-};
-
-const getResources = async () => {
- const response = await fetch("/cli/resource");
-
- if (!response.ok) {
- throw await response.json();
- }
-
- const data = await response.json();
-
- if (data.error) {
- throw data.error;
- }
-
- return data as GetResourcesResponse;
-};
-
-const useResourcesQuery = () => {
- return useQuery(getResources);
-};
-
-export { type Resource, useResourcesQuery };
diff --git a/core/morph/frontend/template/src/admin/common/useScheduledJobsQuery.ts b/core/morph/frontend/template/src/admin/common/useScheduledJobsQuery.ts
deleted file mode 100644
index 52eff98..0000000
--- a/core/morph/frontend/template/src/admin/common/useScheduledJobsQuery.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useQuery } from "./utils/useQuery";
-
-type ScheduledJob = {
- cron: string;
- is_enabled?: boolean;
- timezone?: string;
- variables?: Record;
-};
-
-type GetScheduledJobsResponse = Record;
-
-const getScheduledJobs = async () => {
- const response = await fetch("/cli/morph-project/scheduled-jobs");
-
- if (!response.ok) {
- throw await response.json();
- }
-
- const data = await response.json();
-
- if (data.error) {
- throw data.error;
- }
-
- return data as GetScheduledJobsResponse;
-};
-
-const useScheduledJobsQuery = () => {
- return useQuery(getScheduledJobs);
-};
-
-export { type ScheduledJob, useScheduledJobsQuery };
diff --git a/core/morph/frontend/template/src/admin/common/utils/useQuery.ts b/core/morph/frontend/template/src/admin/common/utils/useQuery.ts
deleted file mode 100644
index c2695cb..0000000
--- a/core/morph/frontend/template/src/admin/common/utils/useQuery.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from "react";
-
-type QueryResult =
- | {
- status: "loading";
- }
- | {
- status: "success";
- data: T;
- }
- | {
- status: "error";
- error: unknown;
- };
-
-export const useQuery = (fetcher: () => T): QueryResult> => {
- const [result, setResult] = React.useState>>({
- status: "loading",
- });
-
- React.useEffect(() => {
- const init = async () => {
- try {
- const data = await fetcher();
-
- setResult({ status: "success", data });
- } catch (error) {
- setResult({ status: "error", error });
- }
- };
-
- init();
- }, [fetcher]);
-
- return result;
-};
diff --git a/core/morph/frontend/template/src/admin/datapipeline/DataPipeline.tsx b/core/morph/frontend/template/src/admin/datapipeline/DataPipeline.tsx
deleted file mode 100644
index abc84a3..0000000
--- a/core/morph/frontend/template/src/admin/datapipeline/DataPipeline.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ReactFlowProvider } from "@xyflow/react";
-import { useResourcesQuery } from "../common/useResourcesQuery";
-
-import "@xyflow/react/dist/style.css";
-import { Flow } from "./Flow";
-import { ResourceDetail } from "./ResourceDetail";
-
-export const DataPipeline = () => {
- const resources = useResourcesQuery();
-
- if (resources.status === "loading") {
- return null;
- }
-
- if (resources.status === "error") {
- throw resources.error;
- }
-
- return (
-
- {/* TODO: Refactor height calculation, avoid hardcoded values */}
-
-
- );
-};
diff --git a/core/morph/frontend/template/src/admin/datapipeline/Flow.tsx b/core/morph/frontend/template/src/admin/datapipeline/Flow.tsx
deleted file mode 100644
index 6601869..0000000
--- a/core/morph/frontend/template/src/admin/datapipeline/Flow.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ReactFlow, Background, Node, Edge } from "@xyflow/react";
-import dagre from "@dagrejs/dagre";
-import { Resource } from "../common/useResourcesQuery";
-
-import "@xyflow/react/dist/style.css";
-import { ResourceNode } from "./ResourceNode";
-
-const nodeTypes = {
- resource: ResourceNode,
-};
-
-export const Flow = ({ resources }: { resources: Resource[] }) => {
- const nodes = convertResourcesToNodes(resources);
- const edges = convertResourcesToEdges(resources);
-
- return (
-
-
-
- );
-};
-
-const convertResourcesToNodes = (resources: Resource[]): Node[] => {
- const WIDTH = 200;
- const HEIGHT = 40;
-
- const graph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
-
- graph.setGraph({ rankdir: "TB", ranksep: HEIGHT, nodesep: WIDTH / 2 });
-
- resources.forEach((resource) =>
- graph.setNode(resource.alias, {
- width: WIDTH,
- height: HEIGHT,
- })
- );
-
- resources.forEach((resource) => {
- resource.data_requirements?.forEach((parentAlias) => {
- graph.setEdge(parentAlias, resource.alias);
- });
- });
-
- dagre.layout(graph);
-
- const nodes: ResourceNode[] = resources.map((resource) => {
- return {
- id: resource.alias,
- type: "resource",
- position: {
- x: graph.node(resource.alias).x,
- y: graph.node(resource.alias).y,
- },
- width: WIDTH,
- height: HEIGHT,
- connectable: false,
- draggable: false,
- handles: [],
- data: { resource },
- };
- });
-
- return nodes;
-};
-
-const convertResourcesToEdges = (resources: Resource[]): Edge[] => {
- return resources.reduce((edges, resource) => {
- const addedEdges =
- resource.data_requirements?.map((parentAlias) => ({
- id: `${parentAlias}-${resource.alias}`,
- source: parentAlias,
- target: resource.alias,
- })) ?? [];
-
- return [...edges, ...addedEdges];
- }, [] as Edge[]);
-};
diff --git a/core/morph/frontend/template/src/admin/datapipeline/ResourceDetail.tsx b/core/morph/frontend/template/src/admin/datapipeline/ResourceDetail.tsx
deleted file mode 100644
index 6ec9077..0000000
--- a/core/morph/frontend/template/src/admin/datapipeline/ResourceDetail.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { Node, useOnSelectionChange } from "@xyflow/react";
-import React from "react";
-import { useScheduledJobsQuery } from "../common/useScheduledJobsQuery";
-import { Resource } from "../common/useResourcesQuery";
-
-export const ResourceDetail = ({ resources }: { resources: Resource[] }) => {
- const [selectedAlias, setSelectedAlias] = React.useState(null);
-
- const onChange = React.useCallback(({ nodes }: { nodes: Node[] }) => {
- if (nodes.length === 0) {
- setSelectedAlias(null);
- } else {
- setSelectedAlias(nodes[0].id);
- }
- }, []);
-
- useOnSelectionChange({
- onChange,
- });
-
- if (!selectedAlias) {
- return (
-
-
Select cell to view details
-
- );
- }
-
- return (
-
-
{selectedAlias}
-
- {/* */}
-
- );
-};
-
-const BasicInfo = ({
- alias,
- resources,
-}: {
- alias: string;
- resources: Resource[];
-}) => {
- const resource = resources.find((r) => r.alias === alias);
-
- if (!resource) {
- return Something went wrong
;
- }
-
- return (
-
-
- Defined at {resource.path}
-
-
- );
-};
-
-const ScheduledJobs = ({ alias }: { alias: string }) => {
- const scheduledJobs = useScheduledJobsQuery();
-
- if (scheduledJobs.status === "loading") {
- return null;
- }
-
- if (scheduledJobs.status === "error") {
- return Something went wrong
;
- }
-
- const scheduledJobsForAlias = scheduledJobs.data[alias]?.schedules ?? [];
-
- return (
-
-
Scheduled Jobs
- {scheduledJobsForAlias.length === 0 && (
-
No scheduled jobs
- )}
- {scheduledJobsForAlias.map((job, i) => (
-
-
{`${job.cron} (${
- job.timezone ?? "UTC"
- })`}
-
- {job.variables ? (
-
-
- Variables
-
-
-
-
- | Key |
- Value |
-
-
-
- {Object.entries(job.variables).map(([key, value]) => (
-
- | {key} |
- {String(value)} |
-
- ))}
-
-
-
- ) : (
-
- No variables
-
- )}
-
- {i < scheduledJobsForAlias.length - 1 && (
-
- )}
-
- ))}
-
- );
-};
diff --git a/core/morph/frontend/template/src/admin/datapipeline/ResourceNode.tsx b/core/morph/frontend/template/src/admin/datapipeline/ResourceNode.tsx
deleted file mode 100644
index 048d6d3..0000000
--- a/core/morph/frontend/template/src/admin/datapipeline/ResourceNode.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { tv } from "tailwind-variants";
-import { Resource } from "../common/useResourcesQuery";
-import {
- Handle,
- Node,
- NodeProps,
- Position,
- useOnSelectionChange,
-} from "@xyflow/react";
-import React from "react";
-import databaseIcon from "../../assets/icons/database.svg";
-import pythonIcon from "../../assets/icons/python.svg";
-
-export type ResourceNode = Node<
- {
- resource: Resource;
- },
- "resource"
->;
-
-const useSelected = (id: string) => {
- const [selected, setSelected] = React.useState(false);
-
- useOnSelectionChange({
- onChange: React.useCallback(({ nodes }) => {
- setSelected(nodes.some((node) => node.id === id));
- }, []),
- });
-
- return selected;
-};
-
-const ResourceNode = ({ id, data }: NodeProps) => {
- const { resource } = data;
-
- const selected = useSelected(id);
-
- return (
-
-
- {resource.alias}
-
-
-
- );
-};
-
-const Icon = ({ resource }: { resource: Resource }) => {
- if (resource.path.endsWith(".py")) {
- return
;
- }
-
- if (resource.path.endsWith(".sql")) {
- return
;
- }
-
- return null;
-};
-
-const card = tv({
- base: "bg-gray-200 p-2 rounded-md flex align-center justify-center gap-2",
- variants: {
- selected: {
- true: "outline outline-gray-400",
- },
- },
-});
-
-export { ResourceNode };
diff --git a/core/morph/frontend/template/src/assets/icons/database.svg b/core/morph/frontend/template/src/assets/icons/database.svg
deleted file mode 100644
index 0458bd5..0000000
--- a/core/morph/frontend/template/src/assets/icons/database.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/core/morph/frontend/template/src/assets/icons/python.svg b/core/morph/frontend/template/src/assets/icons/python.svg
deleted file mode 100644
index e0e096a..0000000
--- a/core/morph/frontend/template/src/assets/icons/python.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/core/morph/frontend/template/src/error-page.tsx b/core/morph/frontend/template/src/error-page.tsx
deleted file mode 100644
index 170d807..0000000
--- a/core/morph/frontend/template/src/error-page.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from "react";
-
-type ErrorPageProps = React.PropsWithChildren<{
- routes: Array<{ path: string; title: string }>;
-}>;
-
-export const ErrorPage: React.FC = () => {
- return (
-
-
Page Not Found
-
Oops! The page you're looking for doesn't exist or has been moved.
-
Please select an exsiting page from below.
-
-
Trouble shoot
-
- Newly added files are not displayed.
-
-
You need to re-start mdx dev server.
-
- - 1. Close preview window on code editor
- -
- 2. Stop process with putting{" "}
-
- Ctrl + C
- {" "}
- to the terminal
-
- -
- 3. Open preview window on code editor again
-
-
-
-
-
- );
-};
-
-export default ErrorPage;
diff --git a/core/morph/frontend/template/src/main.tsx b/core/morph/frontend/template/src/main.tsx
deleted file mode 100644
index 1eb916c..0000000
--- a/core/morph/frontend/template/src/main.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import "vite/modulepreload-polyfill";
-import { createRoot } from "react-dom/client";
-import { createInertiaApp, Head } from "@inertiajs/react";
-import React, { StrictMode, useMemo } from "react";
-import { PageSkeleton } from "./page-skeleton.tsx";
-import "@morph-data/components/css";
-import { MDXComponents } from "mdx/types";
-import type { Toc } from "@stefanprobst/rehype-extract-toc";
-import { AdminPage } from "./admin/AdminPage.tsx";
-import { ErrorPage } from "./error-page.tsx";
-import { useRefresh } from "@morph-data/components";
-import { mdxComponents } from "./mdx-componens.tsx";
-
-type MDXProps = {
- children?: React.ReactNode;
- components?: MDXComponents;
-};
-
-export type MDXComponent = (props: MDXProps) => JSX.Element;
-
-type PageModule = {
- default: MDXComponent;
- title?: string;
- tableOfContents?: Toc;
-}; // types MDX default export
-type Pages = Record;
-
-const pages: Pages = import.meta.glob(
- "/../../src/pages/**/*.mdx",
- {
- eager: true,
- }
-);
-
-const normalizePath = (filePath: string) => {
- // const relativePath = filePath.replace(/\.mdx$/, "").replace(/^\.\/pages/, "");
- const relativePath = filePath
- .replace(/\.mdx$/, "")
- .replace(/^\.\.\/\.\.\/src\/pages/, "");
-
- return relativePath === "/index" ? "/" : relativePath;
-};
-
-const routes = Object.entries(pages).map(([filePath, module]) => {
- // Extract the exported title from the MDX file
- const title = (() => {
- if (module.title) {
- return String(module.title);
- }
-
- if (module.tableOfContents && module.tableOfContents.length > 0) {
- const firstHeading = module.tableOfContents[0];
- if (firstHeading) {
- return firstHeading.value;
- }
- }
-
- return "Untitled";
- })();
-
- return {
- path: normalizePath(filePath),
- title,
- toc: module.tableOfContents,
- };
-});
-
-document.addEventListener("DOMContentLoaded", () => {
- createInertiaApp<{ showAdminPage: boolean }>({
- resolve: (name) => {
- const pageModule = pages[`../../src/pages/${name}.mdx`];
-
- const WrappedComponent: React.FC<{ showAdminPage: boolean }> = ({
- showAdminPage,
- }) => {
- useRefresh();
-
- const firstHeading = useMemo(() => {
- if (
- pageModule?.tableOfContents &&
- pageModule.tableOfContents.length > 0
- ) {
- return pageModule.tableOfContents[0];
- }
- return null;
- }, []);
-
- const title = pageModule?.title || firstHeading?.value || "Untitled";
-
- return (
- <>
-
-
-
-
- {name === "morph" ? (
-
- ) : pageModule ? (
-
- ) : (
-
- )}
-
- >
- );
- };
-
- return WrappedComponent;
- },
- setup({ el, App, props }) {
- createRoot(el).render(
-
-
-
- );
- },
- }).then(() => {});
-});
diff --git a/core/morph/frontend/template/src/mdx-componens.tsx b/core/morph/frontend/template/src/mdx-componens.tsx
deleted file mode 100644
index ad89a77..0000000
--- a/core/morph/frontend/template/src/mdx-componens.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import {
- Accordion,
- Callout,
- Card,
- DataTable,
- Embed,
- Grid,
- LLM,
- Chat,
- Metrics,
- MetricsGrid,
- Panel,
- Input,
- Select,
- SelectGroup,
- SelectGroupLabel,
- SelectItem,
- SelectSeparator,
- SelectItems,
- DatePicker,
- DateRangePicker,
- Pre,
-} from "@morph-data/components";
-import { MDXComponents } from "mdx/types";
-
-const builtinComponents = {
- DataTable,
- Embed,
- Metrics,
- MetricsGrid,
- Input,
- Select,
- SelectGroup,
- SelectGroupLabel,
- SelectItem,
- SelectSeparator,
- SelectItems,
- Card,
- Grid,
- Panel,
- Accordion,
- Callout,
- LLM,
- Chat,
- DatePicker,
- DateRangePicker,
- pre: Pre,
-} as const;
-
-type PluginModule = {
- components?: Record;
-};
-
-type Plugins = {
- [pluginName: string]: PluginModule;
-};
-
-const plugins: Plugins = Object.entries(
- import.meta.glob(
- "/../../src/plugin/**/react/index.ts",
- {
- eager: true,
- }
- )
-).reduce((acc, [reactEntryPath, module]) => {
- // /path/to/plugin-name/react/index.ts -> plugin-name
- const pluginName = reactEntryPath.match(/plugin\/(.+?)\//)?.[1] ?? "";
- return {
- ...acc,
- [pluginName]: module,
- };
-}, {} as Plugins);
-
-const pluginsComponents = Object.entries(plugins).reduce(
- (mdxComponents, [pluginName, module]) => {
- if (!module.components) {
- return mdxComponents;
- }
-
- return Object.entries(module.components).reduce(
- (mdxComponents, [componentName, component]) => {
- const isComponentNameConflict =
- Object.keys(mdxComponents).includes(componentName);
-
- if (isComponentNameConflict) {
- console.warn(
- `Component name conflict: ${componentName} in plugin ${pluginName}`
- );
- }
-
- return {
- ...mdxComponents,
- [componentName]: component,
- };
- },
- mdxComponents
- );
- },
- {} as Record
-);
-
-export const mdxComponents: MDXComponents = {
- ...builtinComponents,
- ...pluginsComponents,
-};
diff --git a/core/morph/frontend/template/src/mdx.d.ts b/core/morph/frontend/template/src/mdx.d.ts
deleted file mode 100644
index 838b4be..0000000
--- a/core/morph/frontend/template/src/mdx.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module "*.mdx" {
- let MDXComponent: (props) => JSX.Element;
- export default MDXComponent;
-}
diff --git a/core/morph/frontend/template/src/page-skeleton.tsx b/core/morph/frontend/template/src/page-skeleton.tsx
deleted file mode 100644
index f7668e3..0000000
--- a/core/morph/frontend/template/src/page-skeleton.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from "react";
-import {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- Button,
- DropdownMenuSeparator,
- Toc,
-} from "@morph-data/components";
-import { ErrorBoundary, FallbackProps } from "react-error-boundary";
-import { Link } from "@inertiajs/react";
-import { Toc as TocEntries } from "@stefanprobst/rehype-extract-toc";
-
-function fallbackRender({ error }: FallbackProps) {
- // Call resetErrorBoundary() to reset the error boundary and retry the render.
- return (
-
-
Something went wrong:
-
{error.message}
-
- );
-}
-
-type PageSkeletonProps = React.PropsWithChildren<{
- routes: Array<{ path: string; title: string }>;
- title: string;
- showAdminPage: boolean;
- toc?: TocEntries;
-}>;
-
-export const PageSkeleton: React.FC = (props) => {
- return (
-
-
-
-
-
-
-
-
- {props.routes.map((route) => (
-
-
-
- {route.title}
-
-
-
- ))}
- {props.showAdminPage && (
- <>
-
-
- Admin Page
-
- >
- )}
-
-
-
{props.title}
-
-
- Made with
-
-
-
-
-
-
-
-
{props.children}
-
-
-
-
-
-
-
- );
-};
diff --git a/core/morph/frontend/template/src/vite-env.d.ts b/core/morph/frontend/template/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe..0000000
--- a/core/morph/frontend/template/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/core/morph/frontend/template/tsconfig.app.json b/core/morph/frontend/template/tsconfig.app.json
deleted file mode 100644
index 0c89611..0000000
--- a/core/morph/frontend/template/tsconfig.app.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true
- },
- "include": ["src"]
-}
diff --git a/core/morph/frontend/template/tsconfig.json b/core/morph/frontend/template/tsconfig.json
deleted file mode 100644
index 2cb35ca..0000000
--- a/core/morph/frontend/template/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "files": [],
- "references": [
- {
- "path": "./tsconfig.app.json"
- },
- {
- "path": "./tsconfig.node.json"
- }
- ],
-}
diff --git a/core/morph/frontend/template/tsconfig.node.json b/core/morph/frontend/template/tsconfig.node.json
deleted file mode 100644
index 3afdd6e..0000000
--- a/core/morph/frontend/template/tsconfig.node.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true,
- "strict": true,
- "noEmit": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/core/morph/frontend/template/vite.config.ts b/core/morph/frontend/template/vite.config.ts
deleted file mode 100644
index 52f6140..0000000
--- a/core/morph/frontend/template/vite.config.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react-swc";
-import { resolve } from "path";
-import ViteRestart from "vite-plugin-restart";
-import mdx from "@mdx-js/rollup";
-import remarkGfm from "remark-gfm";
-import rehypeExtractToc from "@stefanprobst/rehype-extract-toc";
-import rehypeExtractTocMdxExport from "@stefanprobst/rehype-extract-toc/mdx";
-import rehypeSlug from "rehype-slug";
-
-// https://vitejs.dev/config/
-export default defineConfig((env) => ({
- plugins: [
- react(),
- {
- enforce: "pre",
- ...mdx({
- remarkPlugins: [remarkGfm],
- rehypePlugins: [
- rehypeSlug,
- rehypeExtractToc,
- rehypeExtractTocMdxExport,
- ],
- }),
- },
- ViteRestart({
- restart: ["../../src/pages/**/*"],
- }),
- ],
- base: env.mode === "development" ? "" : "/_vite-static",
- server: {
- host: "0.0.0.0",
- open: false,
- watch: {
- usePolling: true,
- disableGlobbing: false,
- },
- cors: true,
- },
- resolve: {
- alias: {
- "@": resolve(__dirname, "./src"),
- },
- },
- build: {
- outDir: resolve("./dist"),
- assetsDir: "assets",
- target: "es2015",
- manifest: "manifest.json",
- rollupOptions: {
- input: {
- main: resolve("./src/main.tsx"),
- },
- output: {
- entryFileNames: `assets/bundle.js`,
- },
- },
- },
-}));
diff --git a/core/morph/include/Dockerfile b/core/morph/include/Dockerfile
deleted file mode 100644
index 328e1cc..0000000
--- a/core/morph/include/Dockerfile
+++ /dev/null
@@ -1,15 +0,0 @@
-# Base image for Morph Cloud
-FROM public.ecr.aws/i1l4z0u0/morph-data:python${MORPH_PYTHON_VERSION}
-
-# Set working directory
-WORKDIR /var/task
-
-# Install Python dependencies
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt --target "${MORPH_PACKAGE_ROOT}"
-
-# Copy source code and dependencies
-COPY . .
-
-# Command to run the Lambda function
-CMD python "${MORPH_APP_FILE_PATH}"
diff --git a/core/morph/include/starter_template/.dockerignore b/core/morph/include/starter_template/.dockerignore
deleted file mode 100644
index c6c3627..0000000
--- a/core/morph/include/starter_template/.dockerignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# .morph
-.morph/*
-!.morph/frontend/dist
-!.morph/core
-!.morph/meta.json
-
-# node_modules
-node_modules
-package-lock.json
-package.json
-
-.mock_user_context.json
-README.md
-.gitignore
-.env
-
-# Python cache files
-__pycache__/
-*.py[cod]
-*$py.class
-.pytest_cache/
-.coverage
-.mypy_cache/
-.ruff_cache/
diff --git a/core/morph/include/starter_template/.gitignore b/core/morph/include/starter_template/.gitignore
index 7acf8d2..b4386bf 100644
--- a/core/morph/include/starter_template/.gitignore
+++ b/core/morph/include/starter_template/.gitignore
@@ -1,6 +1,9 @@
# Morph config files and directories
.morph
+# Morph build output
+dist
+
# Node.js
node_modules
diff --git a/core/morph/include/starter_template/components.json b/core/morph/include/starter_template/components.json
new file mode 100644
index 0000000..67b484d
--- /dev/null
+++ b/core/morph/include/starter_template/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/pages/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/pages/_components",
+ "utils": "@/pages/_lib/utils",
+ "ui": "@/pages/_components/ui",
+ "lib": "@/pages/_lib",
+ "hooks": "@/pages/_hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/core/morph/include/starter_template/package.json b/core/morph/include/starter_template/package.json
index 52ebc56..fbd4e4b 100644
--- a/core/morph/include/starter_template/package.json
+++ b/core/morph/include/starter_template/package.json
@@ -4,11 +4,23 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "build": "npm run build -w .morph/frontend"
+ "dev": "morph-frontend dev",
+ "build": "morph-frontend build"
},
- "dependencies": {},
- "devDependencies": {},
- "workspaces": [
- ".morph/frontend"
- ]
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.14",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "vite": "^6.2.1"
+ },
+ "dependencies": {
+ "@morph-data/frontend": "0.3.0-beta.8",
+ "class-variance-authority": "^0.7.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-error-boundary": "^5.0.0",
+ "tailwind-merge": "^3.0.2",
+ "tailwindcss": "^4.0.14",
+ "tailwindcss-animate": "^1.0.7"
+ }
}
diff --git a/core/morph/include/starter_template/src/pages/404.tsx b/core/morph/include/starter_template/src/pages/404.tsx
new file mode 100644
index 0000000..4a669ab
--- /dev/null
+++ b/core/morph/include/starter_template/src/pages/404.tsx
@@ -0,0 +1,8 @@
+export default function NotFound() {
+ return (
+ <>
+ 404
+ Page not found
+ >
+ );
+}
diff --git a/core/morph/include/starter_template/src/pages/_app.tsx b/core/morph/include/starter_template/src/pages/_app.tsx
new file mode 100644
index 0000000..57f0dc1
--- /dev/null
+++ b/core/morph/include/starter_template/src/pages/_app.tsx
@@ -0,0 +1,82 @@
+import { Head } from "@morph-data/frontend/components";
+import { TableOfContents } from "./_components/table-of-contents";
+import { Header } from "./_components/header";
+import {
+ usePageMeta,
+ MdxComponentsProvider,
+ Outlet,
+ useRefresh,
+ extractComponents,
+} from "@morph-data/frontend/components";
+import { ErrorBoundary } from "react-error-boundary";
+import { Callout } from "@/pages/_components/ui/callout";
+
+import "./index.css";
+
+const uiComponents = extractComponents(
+ import.meta.glob("./_components/ui/**/*.tsx", {
+ eager: true,
+ })
+);
+
+const morphComponents = extractComponents(
+ import.meta.glob("./_components/*.tsx", {
+ eager: true,
+ })
+);
+
+export default function App() {
+ const pageMeta = usePageMeta();
+
+ useRefresh();
+
+ return (
+ <>
+
+ {pageMeta?.title}
+
+
+
+
+
+
+ {pageMeta && }
+
+
+
+
+
+
+ (
+
+ {typeof error.message === "string"
+ ? error.message
+ : "Something went wrong"}
+
+ )}
+ >
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export const Catch = () => (
+
+ Something went wrong
+
+);
diff --git a/core/morph/include/starter_template/src/pages/_components/header.tsx b/core/morph/include/starter_template/src/pages/_components/header.tsx
new file mode 100644
index 0000000..2be7531
--- /dev/null
+++ b/core/morph/include/starter_template/src/pages/_components/header.tsx
@@ -0,0 +1,86 @@
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from "@/pages/_components/ui/dropdown-menu";
+import { Button } from "@/pages/_components/ui/button";
+import { usePages, Link } from "@morph-data/frontend/components";
+import { PropsWithChildren } from "react";
+
+const Root = ({ children }: PropsWithChildren) => {
+ return {children}
;
+};
+
+const DropDownMenu = () => {
+ const pages = usePages();
+
+ return (
+
+
+
+
+
+ {pages.map((page) => (
+
+
+ {page.title}
+
+
+ ))}
+ {/* {props.showAdminPage && (
+ <>
+
+
+ Admin Page
+
+ >
+ )} */}
+
+
+ );
+};
+
+const PageTitle = ({ title }: { title: string }) => {
+ return {title}
;
+};
+
+const Spacer = () => ;
+
+const MorphLogo = () => (
+
+ Made with
+
+
+
+
+);
+
+export const Header = {
+ Root,
+ DropDownMenu,
+ PageTitle,
+ Spacer,
+ MorphLogo,
+};
diff --git a/core/morph/include/starter_template/src/pages/_components/table-of-contents.tsx b/core/morph/include/starter_template/src/pages/_components/table-of-contents.tsx
new file mode 100644
index 0000000..25ba07c
--- /dev/null
+++ b/core/morph/include/starter_template/src/pages/_components/table-of-contents.tsx
@@ -0,0 +1,64 @@
+import {
+ HoverCard,
+ HoverCardTrigger,
+ HoverCardContent,
+} from "@/pages/_components/ui/hover-card";
+import { Button } from "@/pages/_components/ui/button";
+import { LucideTableOfContents } from "lucide-react";
+import { cn } from "@/pages/_lib/utils";
+import { Toc } from "@morph-data/frontend/components";
+
+export interface TocProps {
+ toc?: Toc;
+ className?: string;
+}
+
+export const TableOfContents: React.FC = ({ toc, className }) => {
+ if (!toc) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ {toc.map((entry) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {toc.map((entry) => (
+
+ ))}
+
+
+
+
+ >
+ );
+};
+
+const Heading = ({ entry }: { entry: Toc[number] }) => {
+ return (
+ <>
+
+ {entry.value}
+
+ {entry.children?.map((child) => (
+
+ ))}
+ >
+ );
+};
diff --git a/core/morph/include/starter_template/src/pages/_lib/utils.ts b/core/morph/include/starter_template/src/pages/_lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/core/morph/include/starter_template/src/pages/_lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/core/morph/include/starter_template/src/pages/index.css b/core/morph/include/starter_template/src/pages/index.css
new file mode 100644
index 0000000..f6a6be0
--- /dev/null
+++ b/core/morph/include/starter_template/src/pages/index.css
@@ -0,0 +1,214 @@
+@import "tailwindcss";
+
+@plugin "tailwindcss-animate";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.145 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.145 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.985 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.396 0.141 25.723);
+ --destructive-foreground: oklch(0.637 0.237 25.331);
+ --border: oklch(0.269 0 0);
+ --input: oklch(0.269 0 0);
+ --ring: oklch(0.439 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.269 0 0);
+ --sidebar-ring: oklch(0.439 0 0);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --animate-accordion-down: accordion-down 0.2s ease-out;
+ --animate-accordion-up: accordion-up 0.2s ease-out;
+
+ @keyframes accordion-down {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--radix-accordion-content-height);
+ }
+ }
+
+ @keyframes accordion-up {
+ from {
+ height: var(--radix-accordion-content-height);
+ }
+ to {
+ height: 0;
+ }
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+
+ /* Pre-defined Styles for user generated contents */
+ .morph-page {
+ h1 {
+ @apply mt-10 scroll-m-20 text-3xl font-bold tracking-tight lg:text-4xl;
+ }
+ h2 {
+ @apply mt-8 scroll-m-20 pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0;
+ }
+ h3 {
+ @apply mt-6 scroll-m-20 text-xl font-semibold tracking-tight;
+ }
+ p {
+ @apply leading-relaxed [&:not(:first-child)]:mt-6;
+ }
+ a {
+ @apply font-medium underline underline-offset-4;
+ &.x-underline {
+ @apply no-underline;
+ }
+ }
+ blockquote {
+ @apply mt-6 border-l-2 pl-6 italic;
+ }
+ ul {
+ @apply my-3 ml-3 list-disc list-inside [&>li]:mt-2;
+ }
+ table {
+ @apply table-auto min-w-full text-sm text-left rtl:text-right py-4;
+ }
+ thead {
+ @apply [&_tr]:border-b;
+ }
+ th {
+ @apply py-2 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px];
+ }
+ tbody {
+ @apply [&_tr:last-child]:border-0;
+ }
+ td {
+ @apply p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px];
+ }
+ figure[data-rehype-pretty-code-figure] {
+ width: 100%;
+ max-width: 100%;
+ min-width: 100%;
+ }
+ pre {
+ @apply my-4;
+ width: 100%;
+ max-width: 100%;
+ min-width: 100%;
+ overflow-x: auto;
+ padding: 1rem 0;
+ border-radius: 0.5rem;
+ box-sizing: border-box;
+
+ [data-line] {
+ padding: 0 1rem;
+ font-size: 0.8rem;
+ }
+
+ > code {
+ display: block;
+ }
+ }
+ :not(pre) > code {
+ @apply font-mono text-sm rounded bg-gray-100 dark:bg-neutral-700 px-1.5 py-1;
+ }
+ }
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/core/morph/include/starter_template/tsconfig.json b/core/morph/include/starter_template/tsconfig.json
new file mode 100644
index 0000000..98111af
--- /dev/null
+++ b/core/morph/include/starter_template/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "@morph-data/frontend/tsconfig.app.json",
+ "compilerOptions": {
+ "composite": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/core/morph/include/starter_template/vite.config.ts b/core/morph/include/starter_template/vite.config.ts
new file mode 100644
index 0000000..e68cbc2
--- /dev/null
+++ b/core/morph/include/starter_template/vite.config.ts
@@ -0,0 +1,13 @@
+import { morph } from "@morph-data/frontend/plugin";
+import { defineConfig } from "vite";
+import path from "path";
+import tailwindcss from "@tailwindcss/vite";
+
+export default defineConfig({
+ plugins: [morph(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});
diff --git a/core/morph/task/api.py b/core/morph/task/api.py
index 13d7eae..9beba4b 100644
--- a/core/morph/task/api.py
+++ b/core/morph/task/api.py
@@ -9,10 +9,9 @@
import click
from dotenv import dotenv_values, load_dotenv
-
from morph.cli.flags import Flags
from morph.task.base import BaseTask
-from morph.task.utils.morph import find_project_root_dir, initialize_frontend_dir
+from morph.task.utils.morph import find_project_root_dir
class ApiTask(BaseTask):
@@ -43,15 +42,10 @@ def __init__(self, args: Flags):
for e_key, e_val in env_vars.items():
os.environ[e_key] = str(e_val)
- # Initialize the frontend directory
- # Copy the frontend template to ~/.morph/frontend if it doesn't exist
- self.frontend_dir = initialize_frontend_dir(self.project_root)
-
# for managing subprocesses
self.processes: List[subprocess.Popen[str]] = []
def _find_available_port(self, start_port: int, max_port: int = 65535) -> int:
-
port = start_port
while port <= max_port:
@@ -119,7 +113,7 @@ def _run_frontend(self) -> None:
try:
subprocess.run(
"npm install",
- cwd=self.frontend_dir,
+ cwd=self.project_root,
shell=True,
check=True,
)
@@ -130,9 +124,9 @@ def _run_frontend(self) -> None:
exit(1)
self._run_process(
- ["npm", "run", "dev", "--port", f"{self.front_port}"],
- cwd=self.frontend_dir,
- is_debug=False,
+ ["npm", "run", "dev", "--", "--port", f"{self.front_port}"],
+ cwd=self.project_root,
+ is_debug=True,
)
def _run_process(
diff --git a/core/morph/task/deploy.py b/core/morph/task/deploy.py
index f2d81f6..3996ff2 100644
--- a/core/morph/task/deploy.py
+++ b/core/morph/task/deploy.py
@@ -16,7 +16,8 @@
from morph.config.project import load_project
from morph.task.base import BaseTask
from morph.task.utils.file_upload import FileWithProgress
-from morph.task.utils.morph import find_project_root_dir, initialize_frontend_dir
+from morph.task.utils.load_dockerfile import get_dockerfile_from_api
+from morph.task.utils.morph import find_project_root_dir
class DeployTask(BaseTask):
@@ -34,11 +35,11 @@ def __init__(self, args: Flags):
sys.exit(1)
# Load morph_project.yml or equivalent
- project = load_project(self.project_root)
- if not project:
+ self.project = load_project(self.project_root)
+ if not self.project:
click.echo(click.style("Project configuration not found.", fg="red"))
sys.exit(1)
- elif project.project_id is None:
+ elif self.project.project_id is None:
click.echo(
click.style(
"Error: No project id found. Please fill project_id in morph_project.yml.",
@@ -46,13 +47,34 @@ def __init__(self, args: Flags):
)
)
sys.exit(1)
- self.package_manager = project.package_manager
+ self.package_manager = self.project.package_manager
# Check Dockerfile existence
- self.dockerfile = os.path.join(self.project_root, "Dockerfile")
- if not os.path.exists(self.dockerfile):
- click.echo(click.style(f"Error: {self.dockerfile} not found", fg="red"))
- sys.exit(1)
+ self.dockerfile_path = os.path.join(self.project_root, "Dockerfile")
+ self.use_custom_dockerfile = os.path.exists(self.dockerfile_path)
+ if self.use_custom_dockerfile:
+ provider = "aws"
+ if (
+ self.project.deployment is not None
+ and self.project.deployment.provider is not None
+ ):
+ provider = self.project.deployment.provider or "aws"
+ if self.project.build is None:
+ dockerfile, dockerignore = get_dockerfile_from_api(
+ "morph", provider, None, None
+ )
+ else:
+ dockerfile, dockerignore = get_dockerfile_from_api(
+ self.project.build.framework or "morph",
+ provider,
+ self.project.build.package_manager,
+ self.project.build.runtime,
+ )
+ with open(self.dockerfile_path, "w") as f:
+ f.write(dockerfile)
+ dockerignore_path = os.path.join(self.project_root, ".dockerignore")
+ with open(dockerignore_path, "w") as f:
+ f.write(dockerignore)
# Check Docker availability
try:
@@ -83,9 +105,6 @@ def __init__(self, args: Flags):
click.echo(click.style(f"Error: {str(e)}", fg="red"))
sys.exit(1)
- # Frontend and backend settings
- self.frontend_dir = initialize_frontend_dir(self.project_root)
-
# Docker settings
self.image_name = f"{os.path.basename(self.project_root)}:latest"
self.output_tar = os.path.join(
@@ -109,7 +128,7 @@ def run(self):
click.echo(click.style("Initiating deployment sequence...", fg="blue"))
# 1. Build the source code
- self._copy_and_build_source()
+ self._build_source()
# 2. Build the Docker image
click.echo(click.style("Building Docker image...", fg="blue"))
@@ -129,6 +148,7 @@ def run(self):
project_id=self.client.project_id,
image_build_log=image_build_log,
image_checksum=image_checksum,
+ config=self.project.model_dump() if self.project else None,
)
except Exception as e:
click.echo(
@@ -390,31 +410,8 @@ def _validate_api_key(self):
)
sys.exit(1)
- def _copy_and_build_source(self):
- click.echo(click.style("Building frontend...", fg="blue"))
- try:
- # Run npm install and build
- subprocess.run(
- ["npm", "install"],
- cwd=self.project_root,
- check=True,
- shell=True if sys.platform == "win32" else False,
- )
- subprocess.run(
- ["npm", "run", "build"],
- cwd=self.project_root,
- check=True,
- shell=True if sys.platform == "win32" else False,
- )
-
- except subprocess.CalledProcessError as e:
- click.echo(click.style(f"Error building frontend: {str(e)}", fg="red"))
- sys.exit(1)
- except Exception as e:
- click.echo(click.style(f"Unexpected error: {str(e)}", fg="red"))
- sys.exit(1)
-
- click.echo(click.style("Building backend...", fg="blue"))
+ def _build_source(self):
+ click.echo(click.style("Compiling morph project...", fg="blue"))
try:
# Compile the morph project
subprocess.run(
@@ -440,7 +437,7 @@ def _build_docker_image(self) -> str:
"-t",
self.image_name,
"-f",
- self.dockerfile,
+ self.dockerfile_path,
self.project_root,
]
if self.no_cache:
diff --git a/core/morph/task/new.py b/core/morph/task/new.py
index 68d95e9..711c466 100644
--- a/core/morph/task/new.py
+++ b/core/morph/task/new.py
@@ -8,12 +8,15 @@
from typing import Optional
import click
-
from morph.cli.flags import Flags
-from morph.config.project import default_initial_project, load_project, save_project
+from morph.config.project import (
+ BuildConfig,
+ default_initial_project,
+ load_project,
+ save_project,
+)
from morph.constants import MorphConstant
from morph.task.base import BaseTask
-from morph.task.utils.morph import initialize_frontend_dir
from morph.task.utils.run_backend.state import MorphGlobalContext
@@ -33,10 +36,6 @@ def __init__(self, args: Flags, project_directory: Optional[str]):
os.makedirs(morph_dir)
click.echo(f"Created directory at {morph_dir}")
- # Initialize the frontend directory
- # Copy the frontend template to ~/.morph/frontend if it doesn't exist
- initialize_frontend_dir(self.project_root)
-
# Select the Python version for the project
self.selected_python_version = self._select_python_version()
@@ -111,42 +110,12 @@ def run(self):
)
project.package_manager = "poetry"
- save_project(self.project_root, project)
-
- # Generate the Dockerfile template
- template_dir = Path(__file__).parents[1].joinpath("include")
- docker_template_file = template_dir.joinpath("Dockerfile")
- if not docker_template_file.exists():
- click.echo(
- click.style(
- f"Template file not found: {docker_template_file}", fg="red"
- )
- )
- click.echo()
- sys.exit(1)
-
- # Generate the Dockerfile with the selected Python version
- dockerfile_path = os.path.join(self.project_root, "Dockerfile")
- try:
- with docker_template_file.open("r", encoding="utf-8") as f:
- dockerfile_content = f.read()
-
- # Replace the placeholder with the selected Python version
- dockerfile_content = dockerfile_content.replace(
- "${MORPH_PYTHON_VERSION}", self.selected_python_version
- )
+ if project.build is None:
+ project.build = BuildConfig()
+ project.build.package_manager = project.package_manager
+ project.build.runtime = f"python{self.selected_python_version}"
- # Write the updated Dockerfile to the project directory
- with open(dockerfile_path, "w") as output_file:
- output_file.write(dockerfile_content)
- except FileNotFoundError as e:
- click.echo(
- click.style(f"Error: Template Dockerfile not found: {e}", fg="red")
- )
- sys.exit(1)
- except IOError as e:
- click.echo(click.style(f"Error: Unable to write Dockerfile: {e}", fg="red"))
- sys.exit(1)
+ save_project(self.project_root, project)
try:
morph_data_version = importlib.metadata.version("morph-data")
@@ -263,6 +232,25 @@ def run(self):
)
sys.exit(1)
+ # Setup Frontend
+ subprocess.run(
+ [
+ "npm",
+ "install",
+ ],
+ cwd=self.project_root,
+ )
+ subprocess.run(
+ [
+ "npx",
+ "shadcn@latest",
+ "add",
+ "--yes",
+ "https://morph-components.vercel.app/r/morph-components.json",
+ ],
+ cwd=self.project_root,
+ )
+
click.echo()
click.echo(click.style("Project setup completed successfully! 🎉", fg="green"))
return True
diff --git a/core/morph/task/utils/load_dockerfile.py b/core/morph/task/utils/load_dockerfile.py
new file mode 100644
index 0000000..cfbbddb
--- /dev/null
+++ b/core/morph/task/utils/load_dockerfile.py
@@ -0,0 +1,43 @@
+from typing import Any, Dict, Optional, Tuple
+
+import requests
+
+
+def get_dockerfile_from_api(
+ framework: str,
+ provider: str,
+ package_manager: Optional[str] = None,
+ runtime: Optional[str] = None,
+) -> Tuple[str, str]:
+ """
+ Fetch dockerfile and dockerignore from the Morph API.
+
+ Args:
+ framework: The framework to get the dockerfile for
+ provider: The provider to get the dockerfile for
+ package_manager: Optional package manager to use
+ runtime: Optional runtime to use
+
+ Returns:
+ Tuple containing (dockerfile, dockerignore)
+ """
+ url = f"https://dockerfile-template.morph-cb9.workers.dev/dockerfile/{framework}"
+
+ params: Dict[str, Any] = {
+ "provider": provider,
+ }
+ if package_manager:
+ params["packageManager"] = package_manager
+ if runtime:
+ params["runtime"] = runtime
+
+ response = requests.get(url, params=params)
+
+ response.raise_for_status()
+
+ data = response.json()
+
+ if "error" in data:
+ raise ValueError(data["error"])
+
+ return data["dockerfile"], data["dockerignore"]
diff --git a/core/morph/task/utils/morph.py b/core/morph/task/utils/morph.py
index a8cf637..c5f573c 100644
--- a/core/morph/task/utils/morph.py
+++ b/core/morph/task/utils/morph.py
@@ -2,13 +2,11 @@
import logging
import os
import re
-import shutil
from pathlib import Path
from typing import List, Optional, Union
-from pydantic import BaseModel
-
from morph.constants import MorphConstant
+from pydantic import BaseModel
IGNORE_DIRS = ["/private/tmp", "/tmp"]
@@ -41,23 +39,6 @@ def find_project_root_dir(abs_filepath: Optional[str] = None) -> str:
)
-def initialize_frontend_dir(project_root: str) -> str:
- """
- Initialize the frontend directory by copying the template frontend directory to the project directory.
- Does nothing if the frontend directory already exists.
- @param project_root:
- @return:
- """
- frontend_template_dir = os.path.join(
- Path(__file__).resolve().parents[2], "frontend", "template"
- )
- frontend_dir = MorphConstant.frontend_dir(project_root)
- if not os.path.exists(frontend_dir):
- os.makedirs(frontend_dir, exist_ok=True)
- shutil.copytree(frontend_template_dir, frontend_dir, dirs_exist_ok=True)
- return frontend_dir
-
-
class Resource(BaseModel):
alias: str
path: str
diff --git a/pyproject.toml b/pyproject.toml
index ccca382..a9a39d3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "morph-data"
-version = "0.2.1"
+version = "0.3.0"
description = "Morph is a python-centric full-stack framework for building and deploying data apps."
authors = ["Morph "]
packages = [