A powerful engine for Next.js projects that serves as a dependency for iGRP Studio. This engine provides functionality to create and manage Next.js applications, pages, components, services, workspaces, and more.
The Next.js Engine is designed to be injected as a dependency in the iGRP Studio. It exposes methods from src/index.ts
that can be invoked to perform various actions such as creating new apps, pages, components, services, workspaces, etc. The engine receives JSON configurations and renders Next.js code, Docker services, or React code snippets, writing them to the target files.
npm install @igrp/igrp-studio-nextjs-engine:latest
npm run build
npm run deploy
nextjs-engine/
├── src/
│ ├── components/ # Component definitions and implementations
│ ├── code_snippets/ # Code snippet definitions
│ ├── docker_services/ # Docker service definitions
│ ├── helpers/ # Helper functions for specific tasks
│ ├── interfaces/ # TypeScript interfaces and types
│ ├── modules/ # Core functionality modules
│ ├── registries/ # Registry implementations
│ ├── schema/ # JSON validation schema definitions
│ ├── utils/ # Utility functions and constants
│ └── index.ts # Main entry point and API exports
├── public/ # Public assets and templates
├── __test__/ # Test files
└── package.json # Project dependencies and scripts
The Next.js Engine provides the following core functionality:
- Creating New Applications: Generate new Next.js applications with the required directory structure, configuration files, and dependencies.
- Page Generation: Create new pages with the specified components, layouts, and functionality.
- Component Management: Add, modify, and delete components within pages.
- Workspace Management: Create and manage workspaces containing multiple projects.
This section provides a detailed explanation of the flow from the API calls in src/index.ts
through each method until the target file is generated.
The newWorkspace
method is the entry point for creating a new workspace. Here's the detailed flow:
-
API Call: The process begins when a client calls the
newWorkspace
function exported fromsrc/index.ts
, providing a workspace configuration object and a base path.// Client code import { newWorkspace } from 'nextjs-engine'; const workspaceConfig = { name: 'my-workspace', description: 'My Next.js Workspace', // Additional configuration... }; await newWorkspace(workspaceConfig, '/path/to/destination');
-
Configuration Validation: The workspace configuration is validated against a JSON schema to ensure it contains all required properties and that they are of the correct type.
-
Directory Creation: The engine creates the workspace directory structure at the specified base path.
- It first checks if the directory exists and is empty.
- If the directory doesn't exist, it creates it.
- If the directory exists but isn't empty, it may throw an error or merge the content based on configuration.
-
Template Processing: The engine processes workspace templates from the
public/templates/workspace
directory.- It uses Handlebars as the templating engine to replace placeholders with actual values from the configuration.
- Templates include files like
igrp-compose.yml
,.igrp.env
, and other configuration files needed for an iGRP Workspace.
-
File Generation: The processed templates are written to the appropriate locations in the workspace directory.
- The engine uses Node.js file system operations to write the files.
- It creates the directory structure if it doesn't exist.
-
Environment Setup: The engine sets up environment variables and other configuration needed for the workspace.
- This includes creating
.env
files, setting up Docker configurations if needed, and configuring any other environment-specific settings.
- This includes creating
The newApp
method is the entry point for creating a new Next.js application. Here's the detailed flow:
-
API Call: The process begins when a client calls the
newApp
function exported fromsrc/index.ts
, providing an application configuration object and a base path.// Client code import { newApp } from 'nextjs-engine'; const appConfig = { name: 'my-app', description: 'My Next.js Application', // Additional configuration... }; await newApp(appConfig, '/path/to/destination');
-
Configuration Validation: The application configuration is validated against a JSON schema to ensure it contains all required properties and that they are of the correct type.
-
Directory Creation: The engine creates the application directory structure at the specified base path.
- It first checks if the directory exists and is empty.
- If the directory doesn't exist, it creates it.
- If the directory exists but isn't empty, it may throw an error or merge the content based on configuration.
-
Base Project Extraction: The engine extracts the ZIP template with the base project files that are not present in the engine as Handlebars templates.
-
Template Processing: The engine processes application templates from the
public/templates/app
directory.- It uses Handlebars as the templating engine to replace placeholders with actual values from the configuration.
- Templates include files like
package.json
,next.config.js
,tsconfig.json
, and other configuration files needed for a Next.js application.
-
File Generation: The processed templates are written to the appropriate locations in the application directory.
- The engine uses Node.js file system operations to write the files.
- It creates the directory structure if it doesn't exist.
The newPage
method is the entry point for generating a page in a Next.js application. Here's the detailed flow:
-
API Call: The process begins when a client calls the
newPage
function exported fromsrc/index.ts
, providing a page configuration object and a base path.// Client code import { newPage } from 'nextjs-engine'; const pageConfig = { name: 'home', route: '/', layout: { // Layout configuration... }, // Additional configuration... }; await newPage(pageConfig, '/path/to/app');
-
Configuration Validation: The page configuration is validated against a JSON schema to ensure it contains all required properties and that they are of the correct type.
-
Path Resolution: The engine resolves the path where the page should be created.
- For Next.js applications, this is typically in the
pages
orapp
directory, depending on the Next.js version and routing system being used.
- For Next.js applications, this is typically in the
-
Component Resolution: The engine resolves any components that should be included in the page.
- This involves looking up component definitions in the component registry.
- Components are instantiated with their properties as specified in the configuration.
-
Layout Processing: The engine processes the layout configuration for the page.
- This includes determining the layout structure, such as headers, footers, sidebars, and main content areas.
- The layout is rendered using a template system, typically Handlebars.
-
Template Processing: The engine processes page templates from the
public/templates/app/page
directory.- It uses Handlebars as the templating engine to replace placeholders with actual values from the configuration.
- Templates include the page component, any imports needed, and the layout structure.
-
Component Rendering: Each component in the page is rendered using its own rendering logic.
- Components are rendered accordingly with their renderer that could be default for basic
div
components, Handlebars for components that requires instantiation and custom for custom components instantiation - Components are rendered recursively, with child components being rendered within their parents.
- The rendered components are inserted into the page template.
- Components are rendered accordingly with their renderer that could be default for basic
-
File Generation: The processed templates are written to the appropriate locations in the application directory.
- The engine uses Node.js file system operations to write the files.
- It creates the directory structure if it doesn't exist.
The newComponent
method is the entry point for creating a new component in a Next.js application. Here's the detailed flow:
-
API Call: The process begins when a client calls the
newComponent
function exported fromsrc/index.ts
, providing a component configuration object and a base path.// Client code import { newComponent } from 'nextjs-engine'; const componentConfig = { name: 'MyButton', type: 'button', properties: { label: 'Click Me', // Additional properties... }, // Additional configuration... }; await newComponent(componentConfig, '/path/to/app');
-
Configuration Validation: The component configuration is validated against a JSON schema to ensure it contains all required properties and that they are of the correct type.
-
Component Type Resolution: The engine resolves the component type from the registry.
- This involves register the component definition in the component registry based on the
name
property. - The component definition includes information about how to render the component, its properties, interactions, styles and any other attributes.
- This involves register the component definition in the component registry based on the
-
Path Resolution: The engine resolves the path where the component should be created.
- For Next.js applications, this is typically in the
components
directory.
- For Next.js applications, this is typically in the
-
Template Processing: The engine processes component templates from the
public/templates/component
directory or from the component's own template.- It uses Handlebars as the templating engine to replace placeholders with actual values from the configuration.
- Templates include the component implementation, any imports needed, and the component's properties.
-
Property Processing: The engine processes the component's properties as specified in the configuration.
- Properties are validated against the component's property schema.
- Default values are applied for any properties not specified in the configuration.
-
File Generation: The processed templates are written to the appropriate locations in the application directory.
- The engine uses Node.js file system operations to write the files.
- It creates the directory structure if it doesn't exist.
The addComponentToPage
method is the entry point for adding a component to an existing page. Here's the detailed flow:
-
API Call: The process begins when a client calls the
addComponentToPage
function exported fromsrc/index.ts
, providing a configuration object and a base path.// Client code import { addComponentToPage } from 'nextjs-engine'; const config = { pageId: 'home', component: { type: 'button', properties: { label: 'Click Me', // Additional properties... }, // Additional configuration... }, }; await addComponentToPage(config, '/path/to/app');
-
Configuration Validation: The configuration is validated against a JSON schema to ensure it contains all required properties and that they are of the correct type.
-
Page Resolution: The engine resolves the page where the component should be added.
- This involves finding the page file based on the
pageId
property. - The page file is parsed to understand its structure.
- This involves finding the page file based on the
-
Component Type Resolution: The engine resolves the component type from the registry.
- This involves looking up the component definition in the component registry based on the
name
property. - The component definition includes information about how to render the component, its properties, and any dependencies.
- This involves looking up the component definition in the component registry based on the
-
Component Rendering: The component is rendered using its own rendering logic.
- The rendered component is prepared for insertion into the page.
-
Page Modification: The engine modifies the page file to include the new component.
- This may involve adding imports for the component.
- The component is inserted at the appropriate location in the page's JSX structure.
-
File Update: The modified page file is written back to disk.
- The engine uses Node.js file system operations to write the file.
The file generation process is a critical part of the Next.js Engine. Here's a detailed explanation of how files are generated:
-
Template Loading: The engine loads templates from the
public/templates
directory or from component-specific templates.- Templates are typically Handlebars templates with placeholders for dynamic content.
-
Data Preparation: The engine prepares the data that will be used to fill the template placeholders.
- This data comes from the configuration objects provided to the API methods.
- The data may be transformed or enhanced before being used in the templates.
-
Template Rendering: The engine renders the templates using Handlebars.
- Placeholders in the templates are replaced with actual values from the prepared data.
- Handlebars helpers may be used to perform more complex transformations or conditionals.
-
Path Resolution: The engine resolves the path where the generated file should be written.
- This involves determining the appropriate directory structure based on the file type and configuration.
-
Directory Creation: The engine creates any directories needed for the file path.
- This ensures that the directory structure exists before attempting to write the file.
-
File Writing: The engine writes the rendered content to the specified file path.
- This is typically done using Node.js file system operations.
- The engine may check if the file already exists and handle conflicts based on configuration.
The engine uses a register pattern for components, making it easy to add and remove components. Each component has:
- index.ts: Contains the component implementation and rendering logic.
- properties.ts: Defines the component's properties, including JSON schema for validation.
Components are registered in the register.ts
file, which makes them available to the engine and exposes their configuration to iGRP Studio.
Similar to components, Docker services use the register pattern:
- Each service has its own directory with an
index.ts
andproperties.ts
file. - The
properties.ts
file contains the properties for a Docker Compose service, exposed as a JSON schema. - Services are registered in the
docker_services/register.ts
file.
Code snippets follow the same register pattern:
- Each snippet has its own directory with an
index.ts
andproperties.ts
file. - Snippets are registered in the
code_snippets/register.ts
file.
The main API is exported from src/index.ts
:
// Create a new workspace
export const newWorkspace = async (
workspaceConfig: WorkspaceConfig,
basePath: string
): Promise<void>;
// Create a new application
export const newApp = async (
baseConfig: AppConfig,
basePath: string
): Promise<void>;
// Create a new page
export const newPage = async (
pageConfig: PageConfig,
basePath: string
): Promise<void>;
// Create a new component
export const newComponent = async (
componentConfig: ComponentConfig,
basePath: string
): Promise<void>;
// Add a component to a page
export const addComponentToPage = async (
config: PageComponentConfig,
basePath: string
): Promise<void>;
// Load application exports configuration
export async function loadAppExports(
basePath: string
): Promise<AppExportsConfig>;
// Register custom components
export const registerComponents = (
config: ComponentRegistrationConfig
): void;
// Register custom Docker services
export const registerServices = (
config: DockerServiceRegistrationConfig
): void;
// Register custom code snippets
export const registerCodeSnippets = (
config: CodeSnippetRegistrationConfig
): void;
Components are configured using the ComponentConfig
interface:
export interface ComponentConfig extends IdentifiableElement {
name: string;
type: string;
properties: Record<string, any>;
children?: ComponentConfig[];
// Additional properties...
}
Pages are configured using the PageConfig
interface:
export interface PageConfig extends IdentifiableElement {
name: string;
route: string;
layout: Layout;
meta?: PageMetaConfig;
// Additional properties...
}
Docker services are configured using the DockerServiceRegisterConfig
interface:
export interface DockerServiceRegisterConfig {
name: string;
properties: Record<string, any>;
// Additional properties...
}
Components are registered using the registerAllComponents
function in src/components/register.ts
. This function registers all built-in components and makes them available to the engine.
Custom components can be registered using the registerComponents
function exported from src/index.ts
.
Each component follows a similar structure:
-
index.ts: Contains the component implementation, including:
- A function to render the component
- Logic to handle component properties
- Template rendering using Handlebars
-
properties.ts: Defines the component's properties, including:
- JSON schema for validation
- Default values
- Property descriptions
Components can have variants, which are different configurations of the same component. Variants are defined in the component's registration and can be used to provide different styles or behaviors for the component.
Docker services are registered using the registerAllServices
function in src/docker_services/register.ts
. This function registers all built-in services and makes them available to the engine.
Each Docker service follows a similar structure:
-
index.ts: Contains the service implementation, including:
- A function to render the service configuration
- Logic to handle service properties
- Template rendering using Handlebars
-
properties.ts: Defines the service's properties, including:
- JSON schema for validation
- Default values
- Property descriptions
Code snippets are registered using the registerAllCodeSnippets
function in src/code_snippets/register.ts
. This function registers all built-in snippets and makes them available to the engine.
Each code snippet follows a similar structure:
-
index.ts: Contains the snippet implementation, including:
- A function to render the snippet
- Logic to handle snippet properties
- Template rendering using Handlebars
-
properties.ts: Defines the snippet's properties, including:
- JSON schema for validation
- Default values
- Property descriptions
The engine provides various utility functions to help with common tasks:
// Replace template placeholders with values
export function replaceTemplate(
template: string,
values: Record<string, any>
): string;
// Get the directory path for a file
export function getDirectoryPath(filePath: string): string;
// Check if a directory is empty
export function checkIfDirectoryIsEmpty(dirPath: string): boolean;
// Load configuration from a file
export function loadConfig(configPath: string): any;
// Load page configuration
export function loadPageConfig(pagePath: string): PageConfig;
import { newApp } from 'nextjs-engine';
const appConfig = {
name: 'my-app',
description: 'My Next.js Application',
// Additional configuration...
};
newApp(appConfig, '/path/to/destination');
import { newPage } from 'nextjs-engine';
const pageConfig = {
name: 'home',
route: '/',
layout: {
// Layout configuration...
},
// Additional configuration...
};
newPage(pageConfig, '/path/to/app');
import { addComponentToPage } from 'nextjs-engine';
const componentConfig = {
pageId: 'home',
component: {
type: 'button',
properties: {
label: 'Click Me',
// Additional properties...
},
// Additional configuration...
},
};
addComponentToPage(componentConfig, '/path/to/app');
import { registerComponents } from 'nextjs-engine';
const customComponentConfig = {
components: [
{
name: 'MyCustomComponent',
type: 'myCustomComponent',
// Additional configuration...
},
],
imports: [
'import { MyCustomComponent } from "@components/myCustomComponent"',
],
// Additional configuration...
};
registerComponents(customComponentConfig);
The Next.js Engine includes a comprehensive suite of unit tests to ensure that all functionality works as expected. Here's how to work with the tests:
To run the tests, use the following command:
npm test
This will run all tests in the __test__
directory using Jest.
To run a specific test file, use:
npm test -- __test__/path/to/test.test.ts
The tests are organized in the __test__
directory, with test files corresponding to the functionality they test. For example:
newWorkspace.test.ts
: Tests for creating new workspacesnewApp.test.ts
: Tests for creating new applicationsnewPage.test.ts
: Tests for creating new pagesnewComponent.test.ts
: Tests for creating new componentsaddComponentToPage.test.ts
: Tests for adding components to pages
Each test file typically includes multiple test cases that cover different aspects of the functionality being tested.
The engine aims for high test coverage to ensure that all functionality is properly tested. You can generate a test coverage report using:
npm run test:coverage
This will generate a coverage report in the coverage
directory, which you can open in a browser to see detailed coverage information.
Contributions to the Next.js Engine are welcome! Please follow these steps:
- Fork the repository
- Create a new branch for your feature or bug fix
- Make your changes
- Write tests for your changes
- Run the tests to ensure they pass
- Submit a pull/merge request