Skip to content

Commit

Permalink
feat: Adds Plugins feature (#2563)
Browse files Browse the repository at this point in the history
## What's the purpose of this pull request?

> [!TIP]
> PT-BR: Para melhor compreensão da feature recomenda-se assisti a
[apresentação feita ao time da Faststore
Experience](https://drive.google.com/file/d/13K9Us2jYjNLbrv6-5jD4QGGa614lakFY/view),
explicando as motivações da feature e uma demo do seu funcionamento.

> [!NOTE]
> This PR might seem extensive, but the majority of the changes are
focused on the `hCMS.ts` and `plugins.ts` files. The other files are
mostly support files for the features and will have their content
overwritten during the `generate` process.

### This PR adds the plugin feature to Faststore.

The Faststore Plugins feature enables the addition of new
functionalities to a store without requiring direct integration into the
Faststore core or the store's source code. This approach not only
maintains source code encapsulation but also extends the extensibility
options already available in Faststore, such as Component Override,
theming, and CMS customization, while introducing new capabilities.

Plugins operate as an intermediary layer between the Faststore core and
the store code. This means that modifications made by a plugin override
core functionality but can still be overridden by the store's
customizations, ensuring flexibility and adaptability.

One notable enhancement brought by this feature is the ability to create
new pages, something previously unsupported directly within the store.
For instance, the Buyer Portal — a B2B solution — requires creating a
management page and modifying existing Faststore components. With
Plugins, such functionality can be implemented in a modular and scalable
way, empowering developers to build solutions without impacting the core
or existing store customizations.

_The plugins feature is in the early stages of development and and
plugins can only be developed by VTEX._

<img width="750" alt="Screenshot 2024-11-14 at 17 54 49"
src="https://github.com/user-attachments/assets/1fe97ac6-9e55-4e25-9afa-575a51a88391">

### Implemented Features

- **New Pages**: Plugins enable the creation of new pages in a store.
- **Plugin Override** Components: New sections can be created via
plugins. These sections override core sections but can, in turn, be
overridden by store-specific sections.
- **hCMS Merge**: The content-types.json and sections.json files are
merged across core, plugin, and store files, with the store files taking
precedence.
- **Theme Merge**: Plugins can add theme overrides and customizations.
These plugin-level theme changes can be further overridden by
store-specific themes.


## How it works?

### Configuring a New Plugin

To add custom functionalities to your Faststore store, you can create a
new plugin. This plugin can include custom pages, UI components, themes,
and CMS configurations, all in a modular way.

#### Plugin Package Structure

The plugin should follow this folder structure:

```plaintext
@faststore/new-plugin/
├── src/                        
│   ├── pages/                  # Custom pages
│   ├── components/             # UI components
│   │   └── index.tsx            # Example UI component
│   ├── theme/                  # Custom themes and styles
│   │   └── index.scss          # Styles file
├── cms/                        
│   ├── faststore/              
│   │   ├── content-types.json  # CMS content types
│   │   └── sections.json       # Custom CMS sections
├── package.json                # Plugin metadata and dependencies
├── yarn.lock                   # Yarn dependencies and versions
├── plugin.config.js            # Plugin configuration
├── tsconfig.json               # TypeScript configuration
```
- `src/`: Contains the plugin source code, such as custom pages
(`pages/`), UI components (`components/index.tsx`), and themes
(`theme/index.scss`). All content within src/ will be copied to the
plugins/ folder inside `.faststore` when the plugin is integrated into
the store.

- `cms/`: Contains configuration files for CMS integration, as
`content-types.json` and `sections.json`.

- `plugin.config.js`: The plugin configuration file where page paths are
defined.

### Dependencies
You can add dependencies to your plugin, such as Faststore packages to
access core and UI functionalities, as well as specific packages for the
framework, such as Next.js 13, if required. These dependencies should be
added to the plugin's package.json, enabling advanced features within
the plugin code.

### Creating a New Page
To create a new page in your plugin, you need to define its
configuration in the `plugin.config.js` file. The configuration
structure should look similar to the example below:

```js
module.exports = {
  name: "poc-plugin",
  pages: {
    "my-account": {
      path: "/my-account",
      appLayout: false,
    },
  },
};

```
#### Key Properties:
- Page Name: In this example, `"my-account"` is the name of the page,
and this should match the name of the file in the pages/ folder (e.g.,
`my-account.tsx`).

```plaintext
@faststore/new-plugin/
├── src/                        
│   ├── pages/                    # Custom pages
│   │   └── my-account.ts   # Example page
```

- `path`: This defines the route where the page will be registered. It
follows the same pattern as the Next.js page router, meaning the page
will be accessible via the defined path (e.g., `/my-account`).
- `appLayout`: This property defines whether the global components (like
the header and footer) will be displayed on the page. Set it to false to
disable the default layout.


#### Page Structure
Each page should have at least the following structure. The loader
function will be executed server-side:

```tsx
// loader function to fetch data
export async function loader() {
  const result = await fetch("http://...");

  return await result.json();
}

// Page component to render data
export default function MyAccount(data: any) {
  return (
    <></>
  );
}

```


### Creating New Sections and Overriding Existing Sections
To create new sections or override existing ones, you should follow a
structure similar to the one used in Faststore stores. The sections
should be placed inside the /components folder and the index.tsx file
must export all your sections as an object, with each section as a
property of that object

#### Section Structure
The new sections will be made available, and any sections with the same
name as the core sections will override those sections.

```plaintext
@faststore/new-plugin/
├── src/                        
│   ├── components/             # UI components
│   │   └── ProductDetails.tsx            # Example override section
│   │   └── ProductInfo.tsx            # Example new section
│   │   └── index.tsx            # File to exports components
```

Here's an example of how to define sections in an index.tsx file:

```tsx
import PersonalInfo from "./PersonalInfo";
import ProductDetails from "./ProductDetails";

const sections = {
  ProductDetails,
  PersonalInfo,
};

export default sections;
```

In this example:

- PersonalInfo: is a new section that has been created.
- ProductDetails: is an override of the default component from
Faststore.

### Customizing CMS via Plugin
To customize the CMS via a plugin, you need to create two files:
sections.json and content-types.json. These files should be placed under
the `/cms/faststore/` folder in your plugin.

File Structure:
```plaintext
@faststore/new-plugin/
├── cms/
│   ├── faststore/
│   │   ├── sections.json
│   │   └── content-types.json
```

- `sections.json`: Defines the sections available in the plugin.
- `content-types.json`: Defines the content types handled by the plugin.


#### Merging Files
To merge your plugin’s CMS files with the store’s files, use the
`cms-sync` command. This will merge the plugin’s `sections.json` and
`content-types.json` with the store’s, giving priority to the store's
content.

```bash
cms-sync
```

This command ensures that:

- New sections and content types from the plugin are added.
- Sections/content types with the same name as the core will override
the default ones.
- The store's customizations overrides all.


### Creating a Theme
To create a theme for your plugin, the process is similar to creating a
theme for a store. The theme should be defined in the
`src/themes/index.scss` file, where all the CSS for the plugin will be
written.

File Structure:
```plaintext
@faststore/new-plugin/
├── src/
│   ├── themes/
│   │   └── index.scss
```

`index.scss`: This is where all the styles for the plugin should be
defined.

#### CSS Loading Order
The CSS from the plugin's `index.scss` will be loaded after the global
styles and before the store's CSS, allowing it to override the store's
styles while respecting the global styles.

### Adding a Plugin to the Store
To add a plugin to your store, you need to follow these steps:

#### 1. Install the Plugin Package
First, install the plugin package in the store’s codebase. For example,
if you are adding the @faststore/plugin-test, run:

```bash
yarn add @faststore/plugin-test
```

#### 2. Update discovery.config.js
In the discovery.config.js file, add the plugin name to the plugins
property. This will register the plugin for use in the store.

```js
Copy code
module.exports = {
  ...
  plugins: [
    "@faststore/plugin-test",
  ],
  ...
}
```

#### 3. Configure Plugin Page Paths (Optional)
You can also configure the path for the plugin's pages in the
discovery.config.js file. For example, to change the default path for
the my-account page, use the following structure:

```js
module.exports = {
  ...
  plugins: [
    {
      "@faststore/plugin-test": {
        pages: { "my-account": { path: "/other-path" } },
      },
    },
  ],
  ...
}
```

This allows you to specify custom paths for any pages defined by the
plugin.

## How to test it?

### Testing Locally
To test your plugin locally, you need to link the core package, CLI, and
the plugin to a starter store. Here's how you can do it:

#### 1. Link the Core, CLI, and Plugin Packages
First, you will need to link the core, CLI, and the plugin to your local
starter store. You can do this using yarn link for each package to the
starter from [this
PR](vtex-sites/starter.store#616).

Link the core and CLI from the current PR 
Link the plugin from [this
repository](https://github.com/vtex/poc-faststore-plugin).

#### 2. Run the Starter Store
Once linked, navigate to the starter store's directory and run the
commands as you normally would in a store:

```bash
yarn install
yarn dev
```

3. Server Restart for Changes
Every time you make a change to the plugin, you will need to restart the
server for the changes to take effect.

For changes made to the core and CLI, ensure they are rebuilt before
testing:

```bash
yarn build
```

### Starters Deploy Preview

### Testing on Preview

To test your plugin on the preview environment, follow these steps:

#### 1. Preview the Starter Store with Plugins
There is an open PR that allows you to preview the store with plugins
running. You can access the preview environment using the following
link:

[Preview Store](https://sfj-c18f478--starter.preview.vtex.app/)

This preview is based on the starter store from the PR
[starter.store/pull/616](vtex-sites/starter.store#616).

#### 2. Check the Main Color Override
On the homepage, you will notice that the main color has been overridden
by red, which is done via the plugin. This demonstrates that the plugin
is successfully applying styles to the store.

<img width="760" alt="Screenshot 2024-11-19 at 10 56 19"
src="https://github.com/user-attachments/assets/8f6a66d6-ddc2-480c-b99f-c6123971747d">

#### 3. Test the my-account Page
You can access the my-account page by navigating to /my-account in the
preview store. On this page, you will see the result of the useSession
hook displayed in blue.
[My Account](https://sfj-c18f478--starter.preview.vtex.app/my-account)

<img width="777" alt="Screenshot 2024-11-19 at 10 56 51"
src="https://github.com/user-attachments/assets/a090aae0-ca65-45e8-9522-da0e9337ed5e">

#### 4. Test the Product Detail Page (PDP)
On the Product Detail Page (PDP), you will see the overridden component
from the plugin. This component displays:

The result of the useSession hook.
A Next.js link to navigate back to the homepage.
You can test this by visiting a PDP, such as:

[Product Detail
Page](https://sfj-c18f478--starter.preview.vtex.app/4k-philips-monitor-99988213/p)

<img width="778" alt="Screenshot 2024-11-19 at 10 57 46"
src="https://github.com/user-attachments/assets/f51c9f5c-e498-47e2-a5c0-af5a68d54082">


## References

[B2BTEAM-1951](https://vtex-dev.atlassian.net/browse/B2BTEAM-1951)

[starter.store/pull/616](vtex-sites/starter.store#616).
[Preview Store](https://sfj-c18f478--starter.preview.vtex.app/)

[POC-Plugin](https://github.com/vtex/poc-faststore-plugin/tree/feat/core-usage)

## Checklist

<em>You may erase this after checking them all 😉</em>

**PR Title and Commit Messages**

- [x] PR title and commit messages follow the [Conventional
Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification
- Available prefixes: `feat`, `fix`, `chore`, `docs`, `style`,
`refactor` and `test`

**PR Description**

- [x] Added a label according to the PR goal - `breaking change`, `bug`,
`contributing`, `performance`, `documentation`..

**Dependencies**

- [x] Committed the `yarn.lock` file when there were changes to the
packages


[B2BTEAM-1951]:
https://vtex-dev.atlassian.net/browse/B2BTEAM-1951?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
ArthurTriis1 authored Jan 22, 2025
1 parent 0877d7a commit 83c1bf9
Show file tree
Hide file tree
Showing 44 changed files with 450 additions and 15 deletions.
39 changes: 32 additions & 7 deletions packages/cli/src/utils/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,75 @@ export const withBasePath = (basepath: string) => {
return path.resolve(process.cwd(), basepath)
}

/*
/*
* This will loop from the basepath until the process.cwd() looking for node_modules/@faststore/core
*
*
* If it reaches process.cwd() (or /, as a safeguard), without finding it, it will throw an exception
*/
const getCorePackagePath = () => {
const coreFromNodeModules = path.join('node_modules', '@faststore', 'core')
const packageFromNodeModules = path.join(
'node_modules',
'@faststore',
'core'
)
const resolvedCwd = path.resolve(process.cwd())

const parents: string[] = []

let attemptedPath
do {
attemptedPath = path.join(resolvedCwd, basepath, ...parents, coreFromNodeModules)
attemptedPath = path.join(
resolvedCwd,
basepath,
...parents,
packageFromNodeModules
)

if (fs.existsSync(attemptedPath)) {
return attemptedPath
}

parents.push('..')
} while (path.resolve(attemptedPath) !== resolvedCwd || path.resolve(attemptedPath) !== '/')
} while (
path.resolve(attemptedPath) !== resolvedCwd ||
path.resolve(attemptedPath) !== '/'
)

throw `Could not find @node_modules on ${basepath} or any of its parents until ${attemptedPath}`
}

const tmpDir = path.join(getRoot(), tmpFolderName)
const userSrcDir = path.join(getRoot(), 'src')
const getPackagePath = (...packagePath: string[]) =>
path.join(getRoot(), 'node_modules', ...packagePath)

return {
getRoot,
getPackagePath,
userDir: getRoot(),
userSrcDir,
userThemesFileDir: path.join(userSrcDir, 'themes'),
userCMSDir: path.join(getRoot(), 'cms', 'faststore'),
userLegacyStoreConfigFile: path.join(getRoot(), 'faststore.config.js'),
userStoreConfigFile: path.join(getRoot(), 'discovery.config.js'),

tmpSeoConfig: path.join(tmpDir, 'next-seo.config.ts'),
tmpFolderName,
tmpDir,
tmpCustomizationsSrcDir: path.join(tmpDir, 'src', 'customizations', 'src'),
tmpThemesCustomizationsFile: path.join(tmpDir, 'src', 'customizations', 'src', 'themes', 'index.scss'),
tmpThemesCustomizationsFile: path.join(
tmpDir,
'src',
'customizations',
'src',
'themes',
'index.scss'
),
tmpThemesPluginsFile: path.join(tmpDir, 'src', 'plugins', 'index.scss'),
tmpCMSDir: path.join(tmpDir, 'cms', 'faststore'),
tmpCMSWebhookUrlsFile: path.join(tmpDir, 'cms-webhook-urls.json'),
tmpPagesDir: path.join(tmpDir, 'src', 'pages'),
tmpPluginsDir: path.join(tmpDir, 'src', 'plugins'),
tmpStoreConfigFile: path.join(
tmpDir,
'src',
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/utils/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ora from 'ora'
import { withBasePath } from './directory'
import { installDependencies } from './dependencies'
import { logger } from './logger'
import { installPlugins } from './plugins'

interface GenerateOptions {
setup?: boolean
Expand Down Expand Up @@ -513,5 +514,7 @@ export async function generate(options: GenerateOptions) {
createCmsWebhookUrlsJsonFile(basePath),
updateNextConfig(basePath),
enableRedirectsMiddleware(basePath),

installPlugins(basePath),
])
}
31 changes: 23 additions & 8 deletions packages/cli/src/utils/hcms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CliUx } from '@oclif/core'
import { readFileSync, existsSync, writeFileSync } from 'fs-extra'

import { withBasePath } from './directory'
import { getPluginName, getPluginsList } from './plugins'

export interface ContentTypeOrSectionDefinition {
id?: string
Expand Down Expand Up @@ -96,7 +97,8 @@ async function confirmUserChoice(
fileName: string
) {
const goAhead = await CliUx.ux.confirm(
`You are about to override default ${fileName.split('.')[0]
`You are about to override default ${
fileName.split('.')[0]
}:\n\n${duplicates
.map((definition) => definition.id || definition.name)
.join('\n')}\n\nAre you sure? [yes/no]`
Expand All @@ -110,7 +112,8 @@ async function confirmUserChoice(
}

export async function mergeCMSFile(fileName: string, basePath: string) {
const { coreCMSDir, userCMSDir, tmpCMSDir } = withBasePath(basePath)
const { coreCMSDir, userCMSDir, tmpCMSDir, getPackagePath } =
withBasePath(basePath)

const coreFilePath = path.join(coreCMSDir, fileName)
const customFilePath = path.join(userCMSDir, fileName)
Expand All @@ -123,32 +126,44 @@ export async function mergeCMSFile(fileName: string, basePath: string) {

let output: ContentTypeOrSectionDefinition[] = coreDefinitions

const plugins = await getPluginsList(basePath)

const pluginCMSFilePaths = plugins.map((plugin) =>
getPackagePath(getPluginName(plugin), 'cms', 'faststore', fileName)
)

const customizations = [...pluginCMSFilePaths, customFilePath].filter(
(pluginCMSFilePath) => existsSync(pluginCMSFilePath)
)

// TODO: create a validation when the CMS files exist but don't have a component for them
if (existsSync(customFilePath)) {
const customFile = readFileSync(customFilePath, 'utf8')
for (const newFilePath of customizations) {
const customFile = readFileSync(newFilePath, 'utf8')

try {
const customDefinitions = JSON.parse(customFile)

const { duplicates, newDefinitions } = splitCustomDefinitions(
coreDefinitions,
output,
customDefinitions,
primaryIdentifierForDefinitions
)

if (duplicates.length) {
await confirmUserChoice(duplicates, fileName)
if (newFilePath === customFilePath) {
await confirmUserChoice(duplicates, fileName)
}

output = [
...dedupeAndMergeDefinitions(
coreDefinitions,
output,
duplicates,
primaryIdentifierForDefinitions
),
...newDefinitions,
]
} else {
output = [...coreDefinitions, ...newDefinitions]
output = [...output, ...newDefinitions]
}
} catch (err) {
if (err instanceof SyntaxError) {
Expand Down
Loading

0 comments on commit 83c1bf9

Please sign in to comment.