{"id":26827929,"url":"https://github.com/vseplet/morph","last_synced_at":"2025-07-13T20:07:58.121Z","repository":{"id":283791668,"uuid":"952921851","full_name":"vseplet/morph","owner":"vseplet","description":"Embeddable fullstack library designed for creating Hypermedia-Driven Applications without a build step, based on HTMX and Hono","archived":false,"fork":false,"pushed_at":"2025-06-04T14:59:49.000Z","size":1929,"stargazers_count":25,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-04T21:23:47.386Z","etag":null,"topics":["bun","deno","embedded","nodejs","react","vue"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vseplet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-03-22T06:38:53.000Z","updated_at":"2025-06-04T14:59:49.000Z","dependencies_parsed_at":"2025-06-04T15:47:49.913Z","dependency_job_id":"a279be62-3efc-410a-b277-30e6791fd619","html_url":"https://github.com/vseplet/morph","commit_stats":null,"previous_names":["vseplet/morph"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/vseplet/morph","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vseplet%2Fmorph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vseplet%2Fmorph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vseplet%2Fmorph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vseplet%2Fmorph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vseplet","download_url":"https://codeload.github.com/vseplet/morph/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vseplet%2Fmorph/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265198361,"owners_count":23726449,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","deno","embedded","nodejs","react","vue"],"created_at":"2025-03-30T12:17:58.442Z","updated_at":"2025-07-13T20:07:58.111Z","avatar_url":"https://github.com/vseplet.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Morph\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./morph.png\" alt=\"Morph mascot\" width=\"200\" /\u003e\n\u003c/p\u003e\n\n[![JSR](https://jsr.io/badges/@vseplet/morph)](https://jsr.io/@vseplet/morph)\n[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/vseplet/morph)](https://github.com/vseplet/morph/pulse)\n[![GitHub last commit](https://img.shields.io/github/last-commit/vseplet/morph)](https://github.com/vseplet/morph/commits/main)\n\n## 👋 ATTENTION!\n\n\u003e This package is under development and will be frequently updated. The author\n\u003e would appreciate any help, advice, and pull requests! Thank you for your\n\u003e understanding 😊\n\n---\n\n**Morph** is an embeddable fullstack library for building\n[Hypermedia-Driven Applications](https://htmx.org/essays/hypermedia-driven-applications/)\nwithout a build step, based on [HTMX](https://htmx.org/) and [Hono](https://hono.dev/).\n\n- [Morph](#morph)\n  - [👋 ATTENTION!](#-attention)\n    - [Core principles:](#core-principles)\n  - [Get started](#get-started)\n    - [Add packages](#add-packages)\n    - [Make main.ts and add imports](#make-maints-and-add-imports)\n    - [Create simple page (for all runtimes)](#create-simple-page-for-all-runtimes)\n    - [Setup server](#setup-server)\n    - [And run](#and-run)\n  - [Documentation](#documentation)\n    - [Templates](#templates)\n    - [Components](#components)\n    - [Client-Side JavaScript](#client-side-javascript)\n    - [Styles](#styles)\n    - [Routing, pages and Hono](#routing-pages-and-hono)\n    - [Layout](#layout)\n    - [Wrapper](#wrapper)\n    - [Meta](#meta)\n    - [Partial and HTMX](#partial-and-htmx)\n    - [RPC](#rpc)\n  - [Conclusion](#conclusion)\n  - [License](#license)\n\nMorph exists for one purpose: to simplify the creation of small, straightforward interfaces while eliminating the need to separate frontend and backend into distinct services (as commonly done with Vue/Nuxt and React/Next). It's perfect for embedding web interfaces into existing codebases, whether that's admin panels, dashboards for CLI utilities, Telegram Web Apps (and similar platforms), or small applications and pet projects.\n\nMorph requires virtually nothing - it doesn't impose project structure, doesn't force you to \"build\" or compile anything. It just works here and now. Morph is ideal for solo developers who don't have the resources to develop and maintain a separate frontend, or simply don't need one. In this sense, Morph is the antithesis of modern frameworks, adapted for working with Deno, NodeJS, and Bun.\n\n### Core principles:\n\n- Each component can call its own API that returns hypertext (other components)\n- All components are rendered on the server and have access to server-side\n  context\n- Components can be rendered and re-rendered independently\n- Components form a hierarchy, can be nested in one another, and returned from\n  APIs\n- Minimal or no client-side JavaScript\n- No build step\n- No need to design API data structures upfront\n- The library can be embedded into any Deno/Node/Bun project\n\n## Get started\n\n[(see full examples)](./examples/)\n\n### Add packages\n\n_**Deno**_\\\n`deno add jsr:@vseplet/morph jsr:@hono/hono`\n\n_**Bun**_\\\n`bunx jsr add @vseplet/morph`\\\n`bun add hono`\n\n_**Node**_\\\n`npx jsr add @vseplet/morph`\\\n`npm i --save hono @hono/node-server`\n\n### Make main.ts and add imports\n\nFirst, import Hono based on your runtime:\n\n_**Deno**_\n\n```ts\nimport { Hono } from \"@hono/hono\";\n```\n\n_**Bun**_\n\n```ts\nimport { Hono } from \"hono\";\n```\n\n_**Node**_\n\n```ts\nimport { serve } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\n```\n\nThen, add Morph imports (same for all runtimes):\n\n```ts\nimport { component, fn, html, js, meta, morph, styled } from \"@vseplet/morph\";\n```\n\n### Create simple page (for all runtimes)\n\n```ts\nconst app = new Hono()\n  .all(\"/*\", async (c) =\u003e\n    await morph\n      .page(\n        \"/\",\n        component(async () =\u003e\n          html`\n            ${meta({ title: \"Hello, World!\" })}\n\n            \u003ch1\u003eHello, World!\u003c/h1\u003e\n\n            \u003cpre class=\"${styled`color:red;`}\"\u003e${(await (await fetch(\n              \"https://icanhazdadjoke.com/\",\n              {\n                headers: {\n                  Accept: \"application/json\",\n                  \"User-Agent\": \"My Fun App (https://example.com)\",\n                },\n              },\n            )).json()).joke}\u003c/pre\u003e\n\n            ${fn(() =\u003e alert(\"Hello!\"))}\n          `\n        ),\n      )\n      .fetch(c.req.raw));\n```\n\n### Setup server\n\n_**Deno**_\n\n```ts\nDeno.serve(app.fetch);\n```\n\n_**Bun**_\n\n```ts\nexport default app;\n```\n\n_**Node**_\n\n```ts\nserve(app);\n```\n\n### And run\n\n_**Deno**_\\\n`deno -A main.ts`\n\n_**Bun**_\\\n`bun main.ts`\n\n_**Node**_\\\n`node --experimental-strip-types main.ts`\n\n## Documentation\n\nKey points to understand:\n\nMorph uses Hono under the hood for routing, middleware functions, and more. All\nroutes are described using Hono's routing syntax.\n\nIn Morph, everything consists of components that return template literals. The\ntemplates are described using the `html` tagged template literal:\nhtml`some html here`.\n\nAll components, templates, and other elements are rendered on the server upon\nrequest. In the future, the rendering results of pages and individual components\nmay be cacheable.\n\n### Templates\n\nAll components in Morph are functions that return template literals. Here's a\nsimple example:\n\n```ts\nhtml`\n  \u003ch1\u003eHello World\u003ch1\u003e\n`\n```\n\nTemplate literals are flexible and support all JavaScript template literal\nfeatures, including nested templates:\n\n```ts\nconst buttonName = \"Click Me\"\n\nhtml`\n  \u003ch1\u003eHello World\u003ch1\u003e\n  ${html`\n    \u003cbutton\u003e${buttonName}\u003c/button\u003e\n  `}\n`\n```\n\nThey can also include functions (including asynchronous ones) that return\ntemplates:\n\n```ts\nconst buttonName = \"Click Me\"\n\nhtml`\n  \u003ch1\u003eHello World\u003ch1\u003e\n  ${async () =\u003e {\n    // some async code here\n    return html`\n      \u003cp\u003eAnd here's some data\u003c/p\u003e\n    `\n  }}\n`\n```\n\n### Components\n\nComponents are the building blocks of Morph applications. They are functions\n(possibly asynchronous) that accept props and return template literals. Pages\nthemselves are also components.\n\nHere's a simple component example:\n\n```ts\nconst cmp = component(\n  async () =\u003e\n    html`\n      \u003cdiv\u003e\n        \u003cp\u003eHello, World\u003c/p\u003e\n      \u003c/div\u003e\n    `,\n);\n```\n\nComponents can accept typed props that are defined using TypeScript generics:\n\n```ts\ncomponent\u003c{ title: string }\u003e(\n  async (props) =\u003e\n    html`\n      \u003ch1\u003e${props.title}\u003c/h1\u003e\n    `,\n);\n```\n\nBesides user-defined props, components have access to default props defined in\nthe MorphPageProps type, including: `request: Request` and\n`headers: Record\u003cstring, string\u003e`. This provides immediate access to request\nheaders, parameters, and other request details during component rendering.\n\nComponents can be composed together:\n\n```ts\nconst h1 = component\u003c{ title: string }\u003e((props) =\u003e\n  html`\n    \u003ch1\u003e${props.title}\u003c/h1\u003e\n  `\n);\n\nconst page = component(() =\u003e\n  html`\n    \u003cpage\u003e\n      ${h1({ title: \"Hello, World\" })}\n    \u003c/page\u003e\n  `\n);\n```\n\nAnd they support array operations for dynamic rendering:\n\n```ts\nconst h1 = component\u003c{ title: string }\u003e((props) =\u003e\n  html`\n    \u003ch1\u003e${props.title}\u003c/h1\u003e\n  `\n);\n\nconst page = component(() =\u003e\n  html`\n    \u003cpage\u003e\n      ${[\"title 1\", \"title 2\"].map((title) =\u003e h1({ title }))}\n    \u003c/page\u003e\n  `\n);\n```\n\n### Client-Side JavaScript\n\nYou can embed JavaScript code that will run on the client side directly in your\ntemplates. Here's a simple example:\n\n```ts\nhtml`\n  \u003cdiv\u003e\n    \u003cp id=\"title\"\u003eHello, World\u003c/p\u003e\n    ${js`document.querySelector('#title').innerHTML = 'LoL';`}\n  \u003c/div\u003e\n`;\n```\n\nThis code will be wrapped in an anonymous function and added to the page's\n`\u003cbody\u003e` right after the main HTML content.\n\nAdditionally, you can define a function that will be transpiled to a string and\ninserted into the page code in a similar way:\n\n```ts\nhtml`\n  \u003cdiv\u003e\n    \u003cp id=\"title\"\u003eHello, World\u003c/p\u003e\n    ${fn(() =\u003e document.querySelector(\"#title\").innerHTML = \"LoL\")}\n  \u003c/div\u003e\n`;\n```\n\n### Styles\n\nNot everything is convenient to describe in separate .css files or (especially)\ninline through style=... In some cases, it might be more convenient to generate\nan entire class at once, and for this purpose, we have the following approach:\n\n```ts\nconst color = \"#0056b3\";\n\nconst buttonStyle = styled`\n  border-radius: 15px;\n  border: 1px solid black;\n  cursor: pointer;\n  font-size: 16px;\n\n  \u0026:hover {\n    background-color: ${color};\n  }\n`;\n\nhtml`\n  \u003cdiv\u003e\n    \u003cbutton class=\"${buttonStyle}\"\u003eClick Me\u003c/button\u003e\n  \u003c/div\u003e\n`;\n```\n\n### Routing, pages and Hono\n\nThe entry point is the `morph` object, which provides methods for creating pages\nand handling their rendering on request:\n\n```ts\nconst website = morph\n  .page(\"/a\", cmpA)\n  .page(\"/b\", cmpB);\n```\n\nThen, using Hono, you can create an application and start serving it:\n\n```ts\nconst app = new Hono()\n  .all(\"/*\", async (c) =\u003e website.fetch(c.req.raw));\n\n// Start the server (implementation varies by runtime)\nDeno.serve(app.fetch); // for Deno\n// export default app; // for Bun\n// serve(app); // for Node.js\n```\n\n### Layout\n\n[Coming soon]\n\n### Wrapper\n\n[Coming soon]\n\n### Meta\n\nA simple mechanism that allows you to set template values from any component. For example, to set the page title:\n\n```ts\nconst cmp = component(\n  async () =\u003e\n    html`\n      ${meta({\n        title: \"Hello, World!\"\n      })}\n      \u003cdiv\u003e\n        \u003cp\u003eHello, World\u003c/p\u003e\n      \u003c/div\u003e\n    `\n);\n```\n\nYou can also add content to the head or body sections:\n\n```ts\nmeta({\n  head: `\u003clink rel=\"stylesheet\" href=\"styles.css\"\u003e` // add CSS\n  bodyEnd: `\u003cscript\u003ealert(\"Hi!\")\u003c/script\u003e`\n})\n```\n\nAdditionally, it allows you to set HTTP headers, status codes, and other response metadata.\n\n### Partial and HTMX\n\nHTMX is a powerful library that enables moving data handling and page/component updates from JavaScript to HTML, seamlessly integrating with HTML syntax. In Morph, you can re-render individual components without reloading the entire page (the component is rendered on the server).\n\nHere's a simple example ([full](./examples/redrawing-component.ts)):\n\n```ts\nconst cmp = component(async (props) =\u003e html`\n  \u003cdiv ${props.hx()} hx-swap=\"outerHTML\" hx-trigger=\"every 1s\"\u003e\n    ${Math.random()}\n  \u003c/div\u003e\n`);\n```\n\nNote the `props.hx()` function - it returns a path that can be used to trigger the component's re-rendering. For more information about `hx-swap` and `hx-trigger` attributes, please refer to the [official HTMX documentation](https://htmx.org/docs/).\n\nTo enable component re-rendering, you need to explicitly register it with the Hono router:\n\n```ts\nmorph\n  .partial(cmp)\n  // .page()\n  // .fetch ...\n```\n\n### RPC\n\n(Coming soon)\n\n## Conclusion\n\nThis project is currently in the prototyping stage. Many things may change, be\nadded, or removed as we work towards the main project goals. I welcome any help\nand contributions:\n\n- Test the library and report issues\n- Study the documentation and suggest improvements\n- Submit pull requests with your changes\n- Share your ideas and feedback\n- Use Morph in your pet projects\n\nFeel free to reach out to me on Telegram for any questions or discussions:\n[@vseplet](https://t.me/vseplet)\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvseplet%2Fmorph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvseplet%2Fmorph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvseplet%2Fmorph/lists"}