mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
feat: add documentation for Modules and Syncing Schemas
This commit is contained in:
262
docs/src/contents/03.developer-guide/03.Modules.mdx
Normal file
262
docs/src/contents/03.developer-guide/03.Modules.mdx
Normal file
@@ -0,0 +1,262 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import CustomLink from '../../components/CustomLink';
|
||||
import Code from '../../components/Code';
|
||||
import Note from '../../components/Note';
|
||||
import Warning from '../../components/Warning';
|
||||
|
||||
###### Modules
|
||||
|
||||
# With Absolute Modularity in Mind
|
||||
|
||||
Lifeforge is your canvas, and modules are the vibrant colors that bring your creations to life. With Lifeforge's modular architecture and your creativity, the possibilities are endless. Being a developer means you can craft your own modules to extend Lifeforge's capabilities. Whether you're looking to add new features, integrate with other tools, or customize the user experience, Lifeforge's modular design makes it easy to do so.
|
||||
|
||||
---
|
||||
|
||||
<section id="module-structure">
|
||||
## Module Structure
|
||||
|
||||
All the modules in LifeForge's codebase is located in the <code>apps/</code> directory at the root of the repository. Each module is contained within its own subdirectory, following a consistent structure to ensure ease of development and maintenance.
|
||||
|
||||
### <div className="flex items-center gap-3 -mb-4"><Icon icon="material-icon-theme:folder-client" /><code className="text-xl!">client/</code></div>
|
||||
|
||||
This directory contains the frontend code for the module. It is built using React and Tailwind CSS, the same technologies used in the main LifeForge client codebase. Here, you can define the pages, components, and styles specific to your module. A UI library called <code>lifeforge-ui</code> is also provided in the <code>packages/</code> directory, which you can use to maintain a consistent design language across your module.
|
||||
|
||||
<CustomLink
|
||||
text="Explore UI Library"
|
||||
to="/developer-guide/ui-library"
|
||||
/>
|
||||
|
||||
### <div className="flex items-center gap-3 -mb-4"><Icon icon="material-icon-theme:folder-server" /><code className="text-xl!">server/</code></div>
|
||||
|
||||
This directory contains the backend code for the module. It is built using ExpressJS, the same framework used in the main LifeForge server codebase as well. Here, you can define the API routes, business logic, and database interactions specific to your module. Everything from the main server codebase is also available for you to use, including the type-safe API constructor <code>forgeController</code> and <code>forgeRouter</code>, as well as the type-safe PocketBase wrapper <code>PBService</code>.
|
||||
|
||||
<CustomLink
|
||||
text="Learn More"
|
||||
to="/developer-guide/building-api"
|
||||
/>
|
||||
|
||||
### <div className="flex items-center gap-3 -mb-4"><Icon icon="material-icon-theme:folder-i18n" /><code className="text-xl!">locales/</code></div>
|
||||
|
||||
This directory contains the localization files for the module. It is structured to support multiple languages, allowing you to provide translations for your module's content. Each language has its own JSON file, where you can define the key-value pairs for the translated strings.
|
||||
|
||||
<CustomLink
|
||||
text="Learn More"
|
||||
to="/developer-guide/localization"
|
||||
/>
|
||||
|
||||
### <div className="flex items-center gap-3 -mb-4"><Icon icon="material-icon-theme:nodejs" /><code className="text-xl!">package.json</code></div>
|
||||
|
||||
This file contains the module's metadata and dependencies. LifeForge's monorepo is managed using **bun**, which supports workspaces natively. The newest version of **bun** is mandatory since the dependencies management relies on features that are only available in the latest versions.
|
||||
|
||||
<Note>
|
||||
In bun version 1.2.x and below, workspace dependencies are shared across all workspaces, meaning that if two workspaces depend on different versions of the same package, bun will only install one version at the root level. However, starting from bun version 1.3.x, workspace dependencies are isolated, allowing each workspace to have its own set of dependencies without conflicts. This change is crucial for maintaining module independence and avoiding dependency mismanagement issues.
|
||||
</Note>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="creating-modules">
|
||||
## Creating Modules
|
||||
|
||||
To create a new module, you can use the <code>ForgeCLI</code> tool, which provides a convenient command-line interface for managing LifeForge instances. The CLI includes a command to scaffold a new module with the necessary directory structure and boilerplate code.
|
||||
|
||||
Simply run the following command in your terminal:
|
||||
|
||||
<Code language="bash">
|
||||
{`bun run forge modules create`}
|
||||
</Code>
|
||||
|
||||
Several prompts will guide you through the setup process, allowing you to customize the module's name, description, and other settings, which should look something like this:
|
||||
|
||||

|
||||
|
||||
### Translation of Module Name and Description
|
||||
|
||||
If you already have the OpenAI API key set in your API Key Vault, the CLI will automatically generate translation for your module name and description in multiple languages using OpenAI's GPT-4o-mini model. Otherwise, you can manually edit the localization files later.
|
||||
|
||||
### Selecting Icon
|
||||
|
||||
When prompted to select an icon for your module, you can choose from all the available icons in the Iconify library. The CLI will prompt for the icon sets and icon name separately using an autocomplete interface. However, it won't show a preview of the icon (it's already a proven concept, so it might be added in the future), so you might want to refer to the [Iconify website](https://icon-sets.iconify.design/) to find the icon you want beforehand.
|
||||
|
||||
### Selecting Boilerplate
|
||||
|
||||
You can choose from several boilerplate templates to kickstart your module development. Each boilerplate comes with pre-configured settings and sample code tailored for different use cases. You can also choose a blank template if you prefer to start from scratch.
|
||||
</section>
|
||||
|
||||
<section id="client-manifest">
|
||||
## Client Manifest
|
||||
|
||||
In each module of LifeForge, the client-side codebase contains a special file named <code>manifest.ts</code> at the root of the <code>client/</code> directory. This file serves as the entry point for the module's client application and defines the module's metadata and configuration.
|
||||
|
||||
When creating a module using the <code>ForgeCLI</code>, a default manifest file will be generated for you with the basic structure and necessary fields. Here's an example of what the manifest file looks like:
|
||||
|
||||
<Code language="typescript">
|
||||
{`// apps/[module]/client/manifest.ts
|
||||
import { lazy } from 'react'
|
||||
import type { ModuleConfig } from 'shared'
|
||||
|
||||
export default {
|
||||
name: 'My Module',
|
||||
icon: 'tabler:cube',
|
||||
routes: {
|
||||
'my-module': lazy(() => import('@'))
|
||||
},
|
||||
category: 'Utilities',
|
||||
} satisfies ModuleConfig
|
||||
`}
|
||||
</Code>
|
||||
|
||||
Shown below are the type definition for the <code>ModuleConfig</code> interface:
|
||||
<Code language="typescript">
|
||||
{`export interface ModuleConfig {
|
||||
// Basic module information
|
||||
name: string
|
||||
icon: string
|
||||
// Global provider component for the module. If provided, it will wrap all routes in the module.
|
||||
provider?:
|
||||
| React.LazyExoticComponent<React.ComponentType<any>>
|
||||
| (() => React.ReactElement)
|
||||
// Define the routes for the module
|
||||
// The key is the route path, and the value is the React component to be rendered
|
||||
// Note: The route path should be relative to the module's base path,
|
||||
// e.g., '/my-route' -> '/my-module/my-route'
|
||||
routes: Record<
|
||||
string,
|
||||
| React.LazyExoticComponent<React.ComponentType<any>>
|
||||
| (() => React.ReactElement)
|
||||
>
|
||||
// Whether the module contains AI-powered features
|
||||
// If true, there will be a little magic icon shown next to the module name in the sidebar
|
||||
hasAI?: boolean
|
||||
// List of API keys required for the module to function properly
|
||||
// Will be checked whenever the module is opened in the client
|
||||
// If any of the required API keys are missing, a warning screen will be shown to the user
|
||||
requiredAPIKeys?: string[]
|
||||
// Optional subsections for organizing routes within the sidebar
|
||||
// Note, the path should be an absolute path, eg: '/my-module/subsection-route'
|
||||
// It can be any valid route across the entire system, not limited to the module itself
|
||||
subsection?: {
|
||||
label: string
|
||||
icon: string
|
||||
path: string
|
||||
}[]
|
||||
// Whether the module should be hidden from the sidebar
|
||||
// Useful for modules that are only meant to be accessed programmatically
|
||||
// through buttons or links outside the sidebar
|
||||
hidden?: boolean
|
||||
// Whether the module should be disabled from being accessed
|
||||
// Disabled modules will not be shown in the sidebar and cannot be accessed, even with direct URL
|
||||
// Useful for modules that require special conditions to be met before they can be used,
|
||||
// like external site integration that requires the URL to be set in the environment variables
|
||||
disabled?: boolean
|
||||
// Category of the module, used for grouping modules in the sidebar
|
||||
// If not specified, the module will be placed in the "Miscellaneous" category by default
|
||||
// Modules with category set to '<START>' will be placed at the very top of the sidebar
|
||||
// Modules with category set to '<END>' will be placed at the very bottom of the sidebar
|
||||
category?: string
|
||||
}
|
||||
|
||||
export interface ModuleCategory {
|
||||
title: string
|
||||
items: ModuleConfig[]
|
||||
}
|
||||
`}
|
||||
</Code>
|
||||
</section>
|
||||
|
||||
<section id="categorizing-modules">
|
||||
## Categorizing Modules
|
||||
|
||||
As mentioned above, each module can be assigned to a specific category using the <code>category</code> field in the manifest file. This categorization helps organize modules in the sidebar, making it easier for users to navigate and find the modules they need.
|
||||
|
||||
All modules with the same category will be grouped together under a common heading in the sidebar and sorted alphabetically within that category. However, when it comes to the order of the categories themselves, there is a special file located at <code>apps/cat.config.json</code> that defines the order of the categories. This file exports an array of category names, which determines the order in which the categories are displayed in the sidebar. Categories that are not listed in this file will be placed at the end of the sidebar, sorted alphabetically.
|
||||
|
||||
There are several special category names that are reserved for specific purposes. Their order is fixed and cannot be changed. Shown below is the order where your categories will be displayed in the sidebar:
|
||||
|
||||
<Code language="plaintext">
|
||||
{`<START>
|
||||
[Your Custom Categories in cat.config.json]
|
||||
Miscellaneous
|
||||
Settings
|
||||
SSO
|
||||
<END>`}
|
||||
</Code>
|
||||
</section>
|
||||
|
||||
<section id="managing-dependencies">
|
||||
## Managing Dependencies
|
||||
|
||||
As mentioned above, each module in LifeForge is treated as an independent unit, which means, each module will have its own set of dependencies defined in its own <code>package.json</code> file. This allows module developers to manage their own dependencies without affecting other modules or the main LifeForge codebase.
|
||||
|
||||
When you install a module using the <code>ForgeCLI</code>, it will automatically install several commonly used dependencies that are more often than not required by most modules. These dependencies include:
|
||||
|
||||
- **@iconify/react** – Icon library for rendering icons from Iconify
|
||||
- **@tanstack/react-query** – Data fetching and state management
|
||||
- **@uidotdev/usehooks** – Collection of useful React hooks
|
||||
- **clsx** – Utility for constructing className strings conditionally
|
||||
- **dayjs** – Lightweight date library for parsing, validating, and formatting dates
|
||||
- **lifeforge-ui** – LifeForge's internal UI component library (workspace dependency)
|
||||
- **react** – Core React library
|
||||
- **react-i18next** – Internationalization framework for React
|
||||
- **react-toastify** – Toast notification library
|
||||
- **shared** – Shared utilities and types across the monorepo (workspace dependency)
|
||||
- **vite** – Build tool and dev server
|
||||
- **zod** – TypeScript-first schema validation library
|
||||
|
||||
These dependencies provide the foundational tools and libraries needed for building robust and feature-rich modules. However, module developers are free to add any additional dependencies they require for their specific module functionality.
|
||||
|
||||
<Note>
|
||||
When adding new dependencies to your module, make sure to run <code>bun install</code> in the respective module directory to ensure that the dependencies are installed in the desired module's context, otherwise they might end up being installed at the root level, causing potential waste of disk space and dependency mismanagement issues.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
For dependencies that are shared across multiple modules, please be mindful of the version compatibility to avoid potential conflicts. It is recommended to use the same version of shared dependencies across all modules whenever possible. For example, if some sort of context provider from an external library is used in multiple modules, make sure that all modules are using the same version of that library to prevent issues related to multiple instances of the context provider being created.
|
||||
</Warning>
|
||||
</section>
|
||||
|
||||
<section id="version-control">
|
||||
## Version Control
|
||||
|
||||
When dealing with modules in a monorepo setup, it's important to consider how version control is managed. In LifeForge, each module is treated as an independent unit, thus each module has its own Git repository. This allows module developers to manage their own versioning, branching, and release cycles without affecting the main LifeForge codebase.
|
||||
|
||||
LifeForge utilizes git submodules to manage the modules within the main repository. When you install a module using the <code>ForgeCLI</code>, it adds the module as a git submodule, pointing to the module's own Git repository. This way, you can easily update or switch to different versions of the module without interfering with other modules or the main codebase.
|
||||
|
||||
By default, the <code>ForgeCLI</code> will install the latest stable release of the module from its Git repository. However, you can also specify a specific branch, tag, or commit hash to install a different version of the module if needed.
|
||||
|
||||
You can verify that the module has been added as a git submodule by checking the <code>.gitmodules</code> file at the root of the main repository. This file contains information about all the submodules, including their paths and URLs. Or, you can run the following command to see the list of submodules:
|
||||
|
||||
<Code language="bash">
|
||||
{`git submodule status`}
|
||||
</Code>
|
||||
|
||||
The result will look something like this:
|
||||
|
||||

|
||||
|
||||
<Warning>
|
||||
When deploying your LifeForge instance, make sure that all modules are in the form of git submodules, or else there might be unexpected issues during deployment.
|
||||
</Warning>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="publishing-modules">
|
||||
## Publishing Modules
|
||||
|
||||
<Note>
|
||||
Publishing modules to the public LifeForge module registry is not yet
|
||||
implemented. For now, you can share your module by pushing your code to a public
|
||||
Git repository with a certain structure, and other users can install it using the
|
||||
<code>ForgeCLI</code> by providing the Git URL.
|
||||
|
||||
But don't worry, module publishing will be available soon!
|
||||
</Note>
|
||||
|
||||
When you are ready to share your module with the world, you can use the <code>ForgeCLI</code> to publish your module to your own GitHub repository. This allows other users to easily install your module in their own LifeForge instances. However, this requires you to have your GitHub CLI configured with the necessary permissions to create repositories on your behalf.
|
||||
|
||||
To publish your module, simply run the following command in your terminal:
|
||||
|
||||
<Code language="bash">
|
||||
{`bun run forge modules publish [module]`}
|
||||
</Code>
|
||||
|
||||
Initially, your module will be created locally with a Git repository separated from the main LifeForge codebase. Once published, the module will be deleted from your local <code>apps/</code> directory, and readded as a git submodule pointing to the newly created repository. This ensures that your module remains independent and can be updated separately from the main LifeForge codebase.
|
||||
</section>
|
||||
@@ -178,18 +178,21 @@ Compare the traditional imperative approach:
|
||||
{`const [title, setTitle] = useState('')
|
||||
const [titleError, setTitleError] = useState('')
|
||||
|
||||
const handleTitleChange = (value: string) => {
|
||||
setTitle(value)
|
||||
if (!value.match(/^[a-zA-Z0-9 ]+$/)) {
|
||||
setTitleError('Invalid title. Only alphanumeric characters and spaces are allowed.')
|
||||
} else {
|
||||
setTitleError('')
|
||||
}
|
||||
const handleTitleChange = (value: string) => {
|
||||
setTitle(value)
|
||||
|
||||
if (!value.match(/^[a-zA-Z0-9 ]+$/)) {
|
||||
setTitleError('Invalid title. Only alphanumeric characters and spaces are allowed.')
|
||||
} else {
|
||||
setTitleError('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<form onSubmit={handleSubmit}>...</form>) `}
|
||||
<form onSubmit={handleSubmit}>
|
||||
...
|
||||
</form>
|
||||
) `}
|
||||
|
||||
</Code>
|
||||
|
||||
@@ -199,11 +202,11 @@ With LifeForge's declarative approach:
|
||||
{`const { formProps } = defineForm<FormData>({
|
||||
...uiConfig
|
||||
})
|
||||
.typesMap({...})
|
||||
.setupFields({...})
|
||||
.initialData({...})
|
||||
.onSubmit({...})
|
||||
.build()
|
||||
.typesMap({...})
|
||||
.setupFields({...})
|
||||
.initialData({...})
|
||||
.onSubmit({...})
|
||||
.build()
|
||||
|
||||
return <Form {...formProps} />
|
||||
`}
|
||||
@@ -287,12 +290,12 @@ Import the <code>defineForm</code> function and the <code>FormModal</code> compo
|
||||
<Code language="tsx">
|
||||
{`import { defineForm, FormModal } from 'lifeforge-ui'
|
||||
|
||||
function MyForm({
|
||||
onClose
|
||||
}: {
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { formProps } = defineForm<FormData>({}).build()
|
||||
function MyForm({
|
||||
onClose
|
||||
}: {
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { formProps } = defineForm<FormData>({}).build()
|
||||
|
||||
return <FormModal {...formProps} />
|
||||
|
||||
@@ -569,7 +572,7 @@ Alternatively, you can use a Zod schema to define the validation rules for the f
|
||||
|
||||
In some cases, you may find yourself needing to explicitly manage the form state. This can be useful if you need fine-grained control over the form values or if you're working with complex form logic, for example populating a field with AI generated content.
|
||||
|
||||
LifeForge uses <code>zustand</code> for form state management. The <code>defineForm</code> function exposes a <code>formStateStore</code> object which is a Zustand store that you can use to get and set the form values.
|
||||
LifeForge uses <code>zustand</code> for form state management. The <code>defineForm</code> function exposes a <code>formStateStore</code> object which is a Zustand store that you can use to get and set the form values. In addition, the <code>onClick</code> handler of the <code>actionButtonProps</code> also exposes the state getter and setter functions as its parameters.
|
||||
|
||||
<Code language="tsx">
|
||||
{`
|
||||
@@ -579,14 +582,15 @@ LifeForge uses <code>zustand</code> for form state management. The <code>defineF
|
||||
...,
|
||||
actionButtonProps: {
|
||||
...,
|
||||
onClick: () => {
|
||||
const { name } = formStateStore.getState()
|
||||
onClick: (data, setData) => {
|
||||
const { name } = data
|
||||
|
||||
const newName = await forgeAPI.myApp.coolStuff.generateCoolerName.mutate({
|
||||
currentName: name
|
||||
})
|
||||
|
||||
formStateStore.setState(() => ({
|
||||
setData((oldData) => ({
|
||||
...oldData,
|
||||
name: newName
|
||||
}))
|
||||
}
|
||||
@@ -594,6 +598,14 @@ LifeForge uses <code>zustand</code> for form state management. The <code>defineF
|
||||
}
|
||||
}).build()
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = formStateStore.subscribe((state) => {
|
||||
console.log('Form state changed:', state)
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
return <FormModal {...formProps} />
|
||||
|
||||
}
|
||||
66
docs/src/contents/03.developer-guide/08.SyncingSchemas.mdx
Normal file
66
docs/src/contents/03.developer-guide/08.SyncingSchemas.mdx
Normal file
@@ -0,0 +1,66 @@
|
||||
import Warning from '../../components/Warning'
|
||||
import Code from '../../components/Code'
|
||||
import Note from '../../components/Note'
|
||||
|
||||
###### Syncing schemas
|
||||
|
||||
# Making Sure Your Schemas Are Up-to-Date
|
||||
|
||||
As a developer working with Lifeforge, it's crucial to ensure that your database schemas (located at <code>apps/[module]/server/schemas.ts</code>) are always in sync with your application's models. Lifeforge provides a straightforward way to synchronize your schemas using the ForgeCLI tool.
|
||||
|
||||
---
|
||||
|
||||
<section id="generating-schemas-from-database">
|
||||
## Generating Schemas from Database
|
||||
|
||||
When you make changes to your database structure, such as adding new tables or modifying existing ones, you do it through the PocketBase admin UI. To reflect these changes in your application's codebase, you need to generate the corresponding Zod schemas. You can do this by running the following command in your terminal:
|
||||
|
||||
<Code language="bash">
|
||||
{`bun run forge db generate-schemas [module]`}
|
||||
</Code>
|
||||
|
||||
Replace <code>[module]</code> with the name of the module for which you want to generate the schemas. If not specified, it will generate schemas for all modules.
|
||||
|
||||
This command will connect to your PocketBase instance, retrieve the current database structure, and generate Zod schemas based on the collections and fields defined in your database. The generated schemas will be saved in the schema file of the respective module.
|
||||
|
||||
The collection metadata will also be saved alongside with the zod schemas, which is used to generate migration scripts that sync the database structure with the codebase.
|
||||
|
||||
<Note>
|
||||
If the schema file for a module does not exist yet, running this command will create a new schema file with the generated schemas.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
### Collection Naming Convention
|
||||
When generating schemas from the database, make sure that your collection name follows the naming convention as follows:
|
||||
|
||||
<Code language="plaintext">
|
||||
{`module_name__collection_name`}
|
||||
</Code>
|
||||
|
||||
Note that everything should be in snake case. This convention is important because the ForgeCLI uses the module name prefix to determine which module the collection belongs to. If the naming convention is not followed, the collection will be ignored during the schema generation process.
|
||||
|
||||
For example, if you have a module named <code>My Blog</code> and a collection named <code>post_entries</code>, the collection should be named <code>my_blog__post_entries</code> in PocketBase for the ForgeCLI to recognize it correctly.
|
||||
|
||||
### Do Not Modify Generated Schemas Manually
|
||||
The Zod schemas generated by the ForgeCLI are automatically created based on the database structure. Therefore, it is strongly advised not to modify these generated schemas manually. Any manual changes made to the schemas may be overwritten the next time you run the schema generation command, leading to potential loss of your changes and inconsistencies between your codebase and database structure.
|
||||
</Warning>
|
||||
</section>
|
||||
|
||||
<section id="applying-schemas-to-database">
|
||||
## Applying Schemas to Database
|
||||
|
||||
When you install or update a module, it is very likely that the module comes with its own set of database schemas that need to be applied to your PocketBase instance, or the module author has made changes to the existing schemas. To apply these schemas to your database, you can use the following command:
|
||||
|
||||
<Code language="bash">
|
||||
{`bun run forge db generate-migrations [module]`}
|
||||
</Code>
|
||||
|
||||
This command will read the collection metadata defined in the module's schema file and generate migration scripts to update your PocketBase database accordingly. If you specify a module name, it will only apply the migrations for that specific module; otherwise, it will apply migrations for all installed modules.
|
||||
|
||||
Don't worry, this script will be run automatically whenever you install or update a module using the ForgeCLI, so you don't have to run it manually every time.
|
||||
|
||||
<Warning>
|
||||
### Backup Your Database
|
||||
To prevent any potential data loss, it is highly recommended to back up your PocketBase database before running the migration command. LifeForge is utilizing the built-in migration system of PocketBase, which is (hopefully) generally safe, but it's always better to be cautious when making changes to your database structure.
|
||||
</Warning>
|
||||
</section>
|
||||
Reference in New Issue
Block a user