diff --git a/docs/src/contents/03.developer-guide/03.Modules.mdx b/docs/src/contents/03.developer-guide/03.Modules.mdx new file mode 100644 index 000000000..ecf0c815a --- /dev/null +++ b/docs/src/contents/03.developer-guide/03.Modules.mdx @@ -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. + +--- + +
+## Module Structure + +All the modules in LifeForge's codebase is located in the apps/ 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. + +###
client/
+ +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 lifeforge-ui is also provided in the packages/ directory, which you can use to maintain a consistent design language across your module. + + + +###
server/
+ +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 forgeController and forgeRouter, as well as the type-safe PocketBase wrapper PBService. + + + +###
locales/
+ +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. + + + +###
package.json
+ +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. + + +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. + + +
+ +
+## Creating Modules + +To create a new module, you can use the ForgeCLI 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: + + +{`bun run forge modules create`} + + +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: + +![](https://raw.githubusercontent.com/Lifeforge-app/lifeforge-docs-media/main/assets/developer-creating-module/creation-flow-cli.webp) + +### 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. +
+ +
+## Client Manifest + +In each module of LifeForge, the client-side codebase contains a special file named manifest.ts at the root of the client/ 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 ForgeCLI, 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: + + +{`// 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 +`} + + +Shown below are the type definition for the ModuleConfig interface: + +{`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.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.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 '' will be placed at the very top of the sidebar + // Modules with category set to '' will be placed at the very bottom of the sidebar + category?: string + } + + export interface ModuleCategory { + title: string + items: ModuleConfig[] + } +`} + +
+ +
+## Categorizing Modules + +As mentioned above, each module can be assigned to a specific category using the category 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 apps/cat.config.json 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: + + +{` +[Your Custom Categories in cat.config.json] +Miscellaneous +Settings +SSO +`} + +
+ +
+## 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 package.json 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 ForgeCLI, 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. + + +When adding new dependencies to your module, make sure to run bun install 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. + + + +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. + +
+ +
+## 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 ForgeCLI, 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 ForgeCLI 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 .gitmodules 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: + + +{`git submodule status`} + + +The result will look something like this: + +![](https://raw.githubusercontent.com/Lifeforge-app/lifeforge-docs-media/main/assets/developer-creating-module/submodule-status.webp) + + +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. + + +
+ +
+## Publishing Modules + + + 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 + ForgeCLI by providing the Git URL. + + But don't worry, module publishing will be available soon! + + +When you are ready to share your module with the world, you can use the ForgeCLI 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: + + +{`bun run forge modules publish [module]`} + + +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 apps/ 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. +
\ No newline at end of file diff --git a/docs/src/contents/03.developer-guide/03.Forms.mdx b/docs/src/contents/03.developer-guide/05.Forms.mdx similarity index 95% rename from docs/src/contents/03.developer-guide/03.Forms.mdx rename to docs/src/contents/03.developer-guide/05.Forms.mdx index 58b472eef..621b03e05 100644 --- a/docs/src/contents/03.developer-guide/03.Forms.mdx +++ b/docs/src/contents/03.developer-guide/05.Forms.mdx @@ -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 ( - -
...
) `} +
+ ... +
+) `} @@ -199,11 +202,11 @@ With LifeForge's declarative approach: {`const { formProps } = defineForm({ ...uiConfig }) - .typesMap({...}) - .setupFields({...}) - .initialData({...}) - .onSubmit({...}) - .build() + .typesMap({...}) + .setupFields({...}) + .initialData({...}) + .onSubmit({...}) + .build() return
`} @@ -287,12 +290,12 @@ Import the defineForm function and the FormModal compo {`import { defineForm, FormModal } from 'lifeforge-ui' -function MyForm({ -onClose -}: { -onClose: () => void; -}) { -const { formProps } = defineForm({}).build() + function MyForm({ + onClose + }: { + onClose: () => void; + }) { + const { formProps } = defineForm({}).build() return @@ -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 zustand for form state management. The defineForm function exposes a formStateStore object which is a Zustand store that you can use to get and set the form values. +LifeForge uses zustand for form state management. The defineForm function exposes a formStateStore object which is a Zustand store that you can use to get and set the form values. In addition, the onClick handler of the actionButtonProps also exposes the state getter and setter functions as its parameters. {` @@ -579,14 +582,15 @@ LifeForge uses zustand for form state management. The 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 zustand for form state management. The defineF } }).build() + useEffect(() => { + const unsubscribe = formStateStore.subscribe((state) => { + console.log('Form state changed:', state) + }) + + return unsubscribe + }, []) + return } diff --git a/docs/src/contents/03.developer-guide/04.BuildingAPI.mdx b/docs/src/contents/03.developer-guide/06.BuildingAPI.mdx similarity index 100% rename from docs/src/contents/03.developer-guide/04.BuildingAPI.mdx rename to docs/src/contents/03.developer-guide/06.BuildingAPI.mdx diff --git a/docs/src/contents/03.developer-guide/05.DatabaseOperations.mdx b/docs/src/contents/03.developer-guide/07.DatabaseOperations.mdx similarity index 100% rename from docs/src/contents/03.developer-guide/05.DatabaseOperations.mdx rename to docs/src/contents/03.developer-guide/07.DatabaseOperations.mdx diff --git a/docs/src/contents/03.developer-guide/08.SyncingSchemas.mdx b/docs/src/contents/03.developer-guide/08.SyncingSchemas.mdx new file mode 100644 index 000000000..662e10c90 --- /dev/null +++ b/docs/src/contents/03.developer-guide/08.SyncingSchemas.mdx @@ -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 apps/[module]/server/schemas.ts) are always in sync with your application's models. Lifeforge provides a straightforward way to synchronize your schemas using the ForgeCLI tool. + +--- + +
+## 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: + + +{`bun run forge db generate-schemas [module]`} + + +Replace [module] 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. + + +If the schema file for a module does not exist yet, running this command will create a new schema file with the generated schemas. + + + + ### Collection Naming Convention + When generating schemas from the database, make sure that your collection name follows the naming convention as follows: + + + {`module_name__collection_name`} + + + 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 My Blog and a collection named post_entries, the collection should be named my_blog__post_entries 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. + +
+ +
+## 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: + + +{`bun run forge db generate-migrations [module]`} + + +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. + + + ### 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. + +
\ No newline at end of file