diff --git a/README.md b/README.md
index 1beae075..b11bc962 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
# Fynd Development Kit
>**Note:** Experimental support for Windows is available, it may not be fully stable.
-
[![NPM Version][npm-image]][npm-url]
[![NPM Downloads][downloads-image]][downloads-url]
@@ -12,12 +11,12 @@
Fynd development Kit (FDK CLI) is a cli tool developed by Fynd to create and update themes, extensions and various other components of the [Fynd Platform](https://platform.fynd.com/).
### Quick Links
-| [Fynd Platform](https://platform.fynd.com/) | [Fynd Partners](https://partners.fynd.com/) | [Partners Documentation](https://partners.fynd.com/help) | [Platform Documentation](https://platform.fynd.com/help) | [Other Projects](#other-fynd-projects) | [Contributing](CONTRIBUTING.md) |
+| [Fynd Platform](https://platform.fynd.com/) | [Fynd Partners](https://partners.fynd.com/) | [Partners Documentation](https://partners.fynd.com/help) | [Platform Documentation](https://platform.fynd.com/help) | [Other Projects](#other-fynd-projects) | [Contributing](CONTRIBUTING.md) |
# Prerequisites
- You must have created a [partner account](https://partners.fynd.com/)
-- You must have created development account [guide](https://partners.fynd.com/help/docs/partners/testing-extension/development-acc)
+- You must have created development account [guide](https://partners.fynd.com/help/docs/partners/testing-extension/development-acc)
- You must have installed [Git](https://github.com/git-guides/install-git), if you don't already have it.
- You must have installed [Nodejs](https://nodejs.org/en/download/package-manager) version 18.X.X or higher version, if you don't already have it.
- Optional Prerequisites
@@ -47,7 +46,7 @@ To see the available extension commands, enter:
```sh
fdk extension
```
-See the the [Command reference](#commands-reference) for syntax details and usage examples of the commands.
+See the the [Command reference](#commands-reference) for syntax details and usage examples of the commands.
@@ -55,8 +54,8 @@ See the the [Command reference](#commands-reference) for syntax details and usag
___
### Global Commands
-| Command | Description |
-| ------------- |-------------|
+| Command | Description |
+| ------------- |-------------|
| [login](#login) | Login user |
| [user](#user) | Shows user details of logged in user |
| [logout](#logout) | Logout user |
@@ -66,8 +65,8 @@ ___
### Theme Commands
-| Command | Description |
-| ------------- |-------------|
+| Command | Description |
+| ------------- |-------------|
| [new](#theme-new) | Create new theme |
| [init](#theme-init) | Clone or download the code of the live website onto your local machine to set up a local development environment for testing and modifications. |
| [serve](#theme-serve) | Initiate theme development on your local machine. Your changes will automatically reflect in the browser whenever you save |
@@ -81,16 +80,26 @@ ___
| [active-context](theme-active-context) | show currently active context |
### Extension Commands
-| Command | Description |
-| ------------- |-------------|
+| Command | Description |
+| ------------- |-------------|
| [init](#extension-init) | Utilize this command to set up a new extension locally, leveraging existing templates of your choice. |
| [preview](#extension-preview-url) | Start the extension development server and provide a tunnel URL to preview the extension on the development company. |
| [pull-env](#extension-pull-env) | Retrieve extension context values from the partners panel and update current extension context. |
| [launch-url](#extension-launch-url) | Get/set extension's lanuch url |
+### Extension Binding Commands
+| Command | Description |
+| ------------- |-------------|
+| [init](#binding-init) | Utilize this command to set up a new extension section binding locally, leveraging existing templates of either Vue 2 or React JS. |
+| [draft](#binding-draft) | Create a draft entry of section binding accessible on dev companies.
+| [publish](#binding-publish) | Publish the bindings across all the companies where extension is installed..
+| [preview](#binding-preview) | Create a tunnel and provide a link to tryout extension on any company.
+| [show-context](#binding-show-context) | Show current extension section context.
+| [clear-context](#binding-clear-context) | Clear current extension section context.
+
### Partner Commands
-| Command | Description |
-| ------------- |-------------|
+| Command | Description |
+| ------------- |-------------|
| [connect](#partner-connect) | Add partner access token so that you don't need to add it explicitly |
### Config Commands
@@ -99,7 +108,7 @@ ___
|--------------|--------------------------------------|
| [set](#config-set-commands) | Set configuration values. |
| [get](#config-get-commands) | Retrieve current configuration values.|
-| [delete](#config-delete-commands) (alias: `rm`) | Delete configuration values.
+| [delete](#config-delete-commands) (alias: `rm`) | Delete configuration values.
@@ -127,8 +136,8 @@ This command allows user to login via partner panel.
fdk login [options]
```
#### **Command Options**
-| Option | Description |
-| ------------- |-------------|
+| Option | Description |
+| ------------- |-------------|
| --host | API host |
| --help | Show help |
| --verbose, -v | enable debug mode |
@@ -217,7 +226,7 @@ fdk theme new [options]
#### **Example**
```sh
-fdk theme new -n [your-theme-name]
+fdk theme new -n [your-theme-name]
```
___
@@ -232,8 +241,8 @@ This command is used to initialize an exisiting theme on your local system.
fdk theme init [options]
```
#### **Command Options**
-| Option | Description |
-| ------------- |-------------|
+| Option | Description |
+| ------------- |-------------|
| --help | Show help |
| --verbose, -v | enable debug mode |
@@ -253,7 +262,7 @@ This command is used to add a new context.
fdk theme context [options]
```
#### **Command Options**
-| Option | Description | Required |
+| Option | Description | Required |
| ------------- |-------------| -------- |
| --name, -n | Context name | Yes |
| --help | Show help | No |
@@ -261,7 +270,7 @@ fdk theme context [options]
#### **Example**
```sh
-fdk theme context -n [context-name]
+fdk theme context -n [context-name]
```
___
@@ -294,8 +303,8 @@ This command is used to run a theme on your local system.
fdk theme serve [options]
```
#### **Command Options**
-| Option | Description |
-| ------------- |-------------|
+| Option | Description |
+| ------------- |-------------|
| --ssr | Enable/disable Server-side rendering |
| --port | Pass custom port number to serve theme. `Default: 5001` |
| --help | Show help |
@@ -356,7 +365,7 @@ ___
This command is used to preview the theme on browser.
#### **Syntax**
```sh
-fdk theme open
+fdk theme open
```
### Extension Commands
Extensions are pluggable snippets of code that can be installed in your applications so improve the feature set of your application. To know more visit - [Fynd Partners](https://partners.fynd.com/)
@@ -374,8 +383,8 @@ This command is used to create a extension's initial code with required dependen
fdk extension init [options]
```
#### **Command Options**
-| Option | Description |
-| ------------- |-------------|
+| Option | Description |
+| ------------- |-------------|
| --target-dir | Target Directory |
| --template | Specify the template you want to use to create the extension |
| --help | Show help |
@@ -460,8 +469,8 @@ This command is used to get or set the launch url of your extension
fdk extension launch-url get/set [options]
```
#### **Command Options**
-| Option | Description |
-| ------------- |-------------|
+| Option | Description |
+| ------------- |-------------|
| --url | URL to be set |
| --api-key | Extension ID |
| --help | Show help |
@@ -477,6 +486,133 @@ fdk extension launch-url set --url [url] --api-key [Extension API Key]
fdk extension launch-url get --api-key [Extension API Key]
```
___
+
+### Extension Binding Commands
+Extensions bindings are reusable components which are pluggable through the theme editor to improve the user interface of your application. These can be used just like theme sections.
+
+Set the active environment before running extension commands
+```sh
+fdk env set -u api.fynd.com
+```
+
+
+
+
+#### **init**
+This command is used to create a basic boilerplate code for extension binding with required dependencies.
+#### ****Syntax****
+```sh
+fdk binding init [options]
+```
+#### **Command Options**
+| Option | Description |
+| ------------- |-------------|
+| -n, --name | (Optional) Name of the section binding |
+| -i, --interface | (Optional) Interface where this binding will be used. Currently, we only support Web Theme. |
+| -f, --framework | (Optional) Runtime framework. Supported values are vue2 and react |
+
+#### **Example**
+```sh
+fdk binding init
+```
+___
+
+
+#### **draft**
+This command is used to register the binding with your development companies for alpha or beta testing.
+
+#### ****Syntax****
+```sh
+fdk binding draft [options]
+```
+
+#### **Command Options**
+| Option | Description |
+| ------------- |-------------|
+| -n, --name | (Optional) Name of the section binding |
+| -f, --framework | (Optional) Runtime framework. Supported values are vue2 and react |
+| -id, --extensionId | (Optional) Extension Id of the current extension. |
+| -org, --organisationId | (Optional) Organisation Id of the current extension. |
+
+
+#### **Example**
+```sh
+fdk binding draft
+```
+___
+
+
+
+#### **publish**
+This command is used to publish the binding across all live companies.
+
+#### ****Syntax****
+```sh
+fdk binding publish [options]
+```
+
+#### **Command Options**
+| Option | Description |
+| ------------- |-------------|
+| -n, --name | (Optional) Name of the section binding |
+| -f, --framework | (Optional) Runtime framework. Supported values are vue2 and react |
+| -id, --extensionId | (Optional) Extension Id of the current extension. |
+| -org, --organisationId | (Optional) Organisation Id of the current extension. |
+
+
+#### **Example**
+```sh
+fdk binding publish
+```
+___
+
+
+
+#### **preview**
+This command will allow developers to locally serve the extension binding which has been added to a live storefront.
+
+#### ****Syntax****
+```sh
+fdk binding preview [options]
+```
+
+#### **Command Options**
+| Option | Description |
+| ------------- |-------------|
+| -n, --name | (Optional) Name of the section binding |
+| -f, --framework | (Optional) Runtime framework. Supported values are vue2 and react |
+| -id, --extensionId | (Optional) Extension Id of the current extension. |
+| -org, --organisationId | (Optional) Organisation Id of the current extension. |
+
+
+#### **Example**
+```sh
+fdk binding preview
+```
+
+___
+
+
+
+#### **show-context**
+This command will allow developers to see the current extension section context.
+
+#### ****Syntax****
+```sh
+fdk binding show-context
+```
+___
+
+
+
+#### **clear-context**
+This command will allow developers to clear the current extension section context.
+
+#### ****Syntax****
+```sh
+fdk binding clear-context
+```
+
### Partner Commands
@@ -488,8 +624,8 @@ This command is used to add your partner access token to update extension detail
fdk partner connect [options]
```
#### **Command Options**
-| Option | Description |
-| ------------- |-------------|
+| Option | Description |
+| ------------- |-------------|
| --help | Show help |
| --verbose, -v | enable debug mode |
diff --git a/extension-section-vue/package.json b/extension-section-vue/package.json
new file mode 100644
index 00000000..a2d3da8c
--- /dev/null
+++ b/extension-section-vue/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "extention",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "type": "module",
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "node index.js",
+ "serve": "vue-cli-service serve",
+ "build": "vue-cli-service build --target lib src/index.js --name extension"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "less-loader": "^12.2.0",
+ "vue": "^2.6.11"
+ },
+ "devDependencies": {
+ "@vue/cli-plugin-babel": "^5.0.7",
+ "@vue/cli-service": "^5.0.7",
+ "@vue/test-utils": "1.1.0"
+ }
+}
diff --git a/extension-section-vue/src/index.js b/extension-section-vue/src/index.js
new file mode 100644
index 00000000..b7082a02
--- /dev/null
+++ b/extension-section-vue/src/index.js
@@ -0,0 +1,11 @@
+import * as ProductListing from './sections/product-listing.vue';
+function exportComponents(components) {
+ return [
+ {
+ name: 'product-listing',
+ label: 'product-listing',
+ component: components[0].default,
+ },
+ ];
+}
+export default exportComponents([ProductListing]);
diff --git a/extension-section-vue/src/sections/product-listing.vue b/extension-section-vue/src/sections/product-listing.vue
new file mode 100644
index 00000000..63a07cc0
--- /dev/null
+++ b/extension-section-vue/src/sections/product-listing.vue
@@ -0,0 +1,104 @@
+
+
+
Header - {{ getHeader }}
+
Products List
+
+
+ No Products Found
+
+
+
+
{{ product.name }}
+ {{ product.slug }}
+
+
+
+
+
+
+
+
+ {
+ "name": "product-listing",
+ "label": "product-listing",
+ "props": [
+ {
+ "type": "text",
+ "id": "heading",
+ "default": "Products",
+ "label": "Heading",
+ "info":"Heading text of the section"
+ }
+ ],
+ "blocks": [],
+ "preset": {}
+ }
+
+
+
diff --git a/extension-section/package.json b/extension-section/package.json
new file mode 100644
index 00000000..3531c8ec
--- /dev/null
+++ b/extension-section/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "extension",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack",
+ "build:dev": "webpack --watch",
+ "dev": "npm run clean && npm run build:dev",
+ "clean": "rm -rf dist/"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@babel/preset-env": "^7.23.2",
+ "@babel/preset-react": "^7.22.15",
+ "babel-loader": "^9.1.2",
+ "css-loader": "^6.7.3",
+ "mini-css-extract-plugin": "^2.7.5",
+ "path": "^0.12.7",
+ "webpack": "^5.76.2",
+ "webpack-cli": "^5.0.1"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0",
+ "react-router-dom": "^6.6.2"
+ }
+}
diff --git a/extension-section/src/components/ProductCard.jsx b/extension-section/src/components/ProductCard.jsx
new file mode 100644
index 00000000..02546c5e
--- /dev/null
+++ b/extension-section/src/components/ProductCard.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import styles from '../styles/style.css';
+
+export function ProductCard({ product }) {
+ return (
+
+ {product.medias.map((media) => (
+
+ ))}
+
{product.name}
+
{product.slug}
+
+ );
+}
diff --git a/extension-section/src/index.jsx b/extension-section/src/index.jsx
new file mode 100644
index 00000000..9f74cd1a
--- /dev/null
+++ b/extension-section/src/index.jsx
@@ -0,0 +1,5 @@
+import * as ProductList from './sections/product-list';
+
+export default {
+ 'product-list': { ...ProductList, },
+}
\ No newline at end of file
diff --git a/extension-section/src/sections/product-list.jsx b/extension-section/src/sections/product-list.jsx
new file mode 100644
index 00000000..9a69ad21
--- /dev/null
+++ b/extension-section/src/sections/product-list.jsx
@@ -0,0 +1,56 @@
+import React, { useEffect } from "react";
+
+import { useGlobalStore, useFPI } from "fdk-core/utils";
+import { Helmet } from "react-helmet-async";
+import styles from "../styles/style.css";
+import { ProductCard } from "../components/ProductCard";
+
+export function Component({ props }) {
+ const fpi = useFPI();
+ const products = useGlobalStore(fpi.getters.PRODUCTS);
+
+ const productItems = products?.data?.items ?? [];
+ useEffect(() => {
+ if (!productItems.length) {
+ fpi.catalog.getProducts({});
+ }
+ }, []);
+
+ const title = props?.title?.value ?? 'Extension Title Default'
+
+ return (
+
+
+ { title }
+
+
Products List
+
+ {!productItems.length ? (
+
No Products
+ ) : (
+
+ {productItems.map((product) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+Component.serverFetch = ({ fpi }) => fpi.catalog.getProducts({});
+
+export const settings = {
+ label: "Product List",
+ name: "product-list",
+ props: [
+ {
+ id: "title",
+ label: "Page Title",
+ type: "text",
+ default: "Extension Title",
+ info: "Page Title",
+ },
+ ],
+ blocks: [],
+};
diff --git a/extension-section/src/styles/style.css b/extension-section/src/styles/style.css
new file mode 100644
index 00000000..012eea84
--- /dev/null
+++ b/extension-section/src/styles/style.css
@@ -0,0 +1,7 @@
+.product {
+ color: red;
+ border: 1px solid red
+}
+.container {
+ display: flex;
+}
\ No newline at end of file
diff --git a/extension-section/webpack.config.js b/extension-section/webpack.config.js
new file mode 100644
index 00000000..aaa34ee0
--- /dev/null
+++ b/extension-section/webpack.config.js
@@ -0,0 +1,110 @@
+const path = require("path");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+
+module.exports = (env) => {
+ const isLocalBuild = env.isLocal;
+ const context = env.context;
+
+ const baseConfig = {
+ mode: isLocalBuild ? 'development' : 'production',
+ entry: path.resolve(context, "src/index.jsx"),
+ resolve: {
+ extensions: ['', '.js', '.jsx'],
+ },
+ module: {
+ rules: [
+ {
+ test: /\.(jsx|js)$/,
+ include: path.resolve(context, "src"),
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: "babel-loader",
+ options: {
+ presets: [
+ [
+ "@babel/preset-env",
+ {
+ targets: "defaults",
+ },
+ ],
+ "@babel/preset-react",
+ ],
+ },
+ },
+ ],
+ },
+ {
+ test: /\.css$/i,
+ use: [
+ MiniCssExtractPlugin.loader,
+ {
+ loader: "css-loader",
+ options: {
+ modules: {
+ localIdentName: isLocalBuild
+ ? "[path][name]__[local]--[hash:base64:5]"
+ : "[hash:base64:5]",
+ },
+ },
+ },
+ ],
+ exclude: /\.global\.css$/,
+ },
+ {
+ test: /\.css$/i,
+ use: [
+ MiniCssExtractPlugin.loader,
+ {
+ loader: "css-loader",
+ options: {
+ modules: false,
+ },
+ },
+ ],
+ include: /\.global\.css$/,
+ },
+ {
+ test: /\.less$/i,
+ use: [
+ // compiles Less to CSS
+ MiniCssExtractPlugin.loader,
+ {
+ loader: "css-loader",
+ options: {
+ modules: false,
+ },
+ },
+ "less-loader",
+ ],
+ include: /\.global\.less$/,
+ },
+ {
+ test: /\.less$/i,
+ use: [
+ // compiles Less to CSS
+ MiniCssExtractPlugin.loader,
+ {
+ loader: "css-loader",
+ options: {
+ modules: {
+ localIdentName: isLocalBuild
+ ? "[path][name]__[local]--[hash:base64:5]"
+ : "[hash:base64:5]",
+ },
+ },
+ },
+ "less-loader",
+ ],
+ exclude: /\.global\.less$/,
+ },
+ ],
+ },
+ plugins: [
+ new MiniCssExtractPlugin({
+ filename: `${env.bundleName}.umd.min.css`,
+ }),
+ ]
+ };
+ return baseConfig;
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 1b7372f2..4d6545f5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@gofynd/fdk-cli",
- "version": "6.0.0",
+ "version": "6.1.0",
"main": "index.js",
"license": "MIT",
"bin": {
@@ -16,7 +16,11 @@
"src/**/*.js",
"template",
"react-template",
- "react-theme-template"
+ "react-theme-template",
+ "extension-section",
+ "extension-section-vue",
+ "sample-upload.js",
+ "sample-upload.jpeg"
],
"scripts": {
"clean": "rimraf ./dist",
@@ -52,12 +56,13 @@
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.3",
- "typescript": "^5.5.4",
- "webpack": "^5.93.0"
+ "typescript": "^5.5.4"
},
"dependencies": {
"@babel/core": "^7.24.9",
"@babel/plugin-transform-modules-commonjs": "^7.24.8",
+ "@babel/preset-env": "^7.23.5",
+ "@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.24.7",
"@gofynd/fp-signature": "^1.0.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
@@ -97,6 +102,7 @@
"ora": "^5.4.1",
"query-string": "^7.1.3",
"react": "^18.3.1",
+ "react-dom": "^18.3.1",
"react-refresh": "^0.14.2",
"react-router-dom": "^6.25.1",
"rimraf": "^3.0.2",
@@ -108,7 +114,9 @@
"terminal-link": "^1.3.0",
"terser-webpack-plugin": "^5.3.10",
"url-join": "^4.0.1",
+ "uuid": "^10.0.0",
"vue-template-compiler": "^2.7.16",
+ "webpack": "^5.93.0",
"webpack-dev-middleware": "^6.1.3",
"webpack-hot-middleware": "^2.26.1",
"webpack-merge": "^5.10.0",
@@ -116,6 +124,6 @@
"winston": "^3.13.1"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
}
}
diff --git a/sample-upload.jpeg b/sample-upload.jpeg
new file mode 100644
index 00000000..e2997fa2
--- /dev/null
+++ b/sample-upload.jpeg
@@ -0,0 +1,2 @@
+// ! DO NOT REMOVE
+// This file is used to get CDN base path
\ No newline at end of file
diff --git a/sample-upload.js b/sample-upload.js
new file mode 100644
index 00000000..e2997fa2
--- /dev/null
+++ b/sample-upload.js
@@ -0,0 +1,2 @@
+// ! DO NOT REMOVE
+// This file is used to get CDN base path
\ No newline at end of file
diff --git a/src/__tests__/theme.spec.ts b/src/__tests__/theme.spec.ts
index 8e4a158c..c0138db6 100644
--- a/src/__tests__/theme.spec.ts
+++ b/src/__tests__/theme.spec.ts
@@ -324,7 +324,7 @@ describe('Theme Commands', () => {
)}`,
).reply(200, { name: 'Emerge' });
-
+
mock.onGet(`${URLS.GET_ORGANIZATION_DETAILS()}`).reply(200, organizationData);
configStore.delete(CONFIG_KEYS.ORGANIZATION)
diff --git a/src/__tests__/themeContext.spec.ts b/src/__tests__/themeContext.spec.ts
index 35415d3a..78f04747 100644
--- a/src/__tests__/themeContext.spec.ts
+++ b/src/__tests__/themeContext.spec.ts
@@ -74,9 +74,9 @@ describe('Theme Context Commands', () => {
setEnv();
program = await init('fdk');
const mock = new MockAdapter(axios);
-
+
configStore.set(CONFIG_KEYS.ORGANIZATION, organizationData._id)
-
+
mock.onGet('https://api.fyndx1.de/service/application/content/_healthz').reply(200);
mock.onGet(
diff --git a/src/commands/binding/binding-builder.ts b/src/commands/binding/binding-builder.ts
new file mode 100644
index 00000000..def7e065
--- /dev/null
+++ b/src/commands/binding/binding-builder.ts
@@ -0,0 +1,54 @@
+import { Command } from 'commander';
+import ExtensionSection from '../../lib/ExtensionSection';
+
+export default function bindingCommandBuilder() {
+ const binding = new Command('binding').description(
+ 'Extension Binding Commands',
+ );
+ binding
+ .command('init')
+ .description('Create a new section binding boilerplate')
+ .option('-n, --name [name]', 'Bundle Name')
+ .option('-i, --interface [interface]', 'Interface')
+ .option('-f, --framework [framework]', 'Compatible Framework')
+ .asyncAction(ExtensionSection.initExtensionBinding);
+
+ binding
+ .command('draft')
+ .description('Draft extension section')
+ .option('-id, --extensionId [extensionId]', 'Extension ID')
+ .option('-org, --organisationId [organisationId]', 'Organisation ID')
+ .option('-n, --name [name]', 'Bundle Name')
+ .option('-f, --framework [framework]', 'Compatible Framework')
+ .asyncAction(ExtensionSection.draftExtensionBindings);
+
+ binding
+ .command('publish')
+ .description('Publish extension section')
+ .option('-id, --extensionId [extensionId]', 'Extension ID')
+ .option('-org, --organisationId [organisationId]', 'Organisation ID')
+ .option('-n, --name [name]', 'Bundle Name')
+ .option('-f, --framework [framework]', 'Compatible Framework')
+ .asyncAction(ExtensionSection.publishExtensionBindings);
+
+ binding
+ .command('preview')
+ .description('Serve extension sections')
+ .option('-id, --extensionId [extensionId]', 'Extension ID')
+ .option('-org, --organisationId [organisationId]', 'Organisation ID')
+ .option('-n, --name [name]', 'Bundle Name')
+ .option('-f, --framework [framework]', 'Compatible Framework')
+ .asyncAction(ExtensionSection.previewExtension);
+
+ binding
+ .command('clear-context')
+ .description('Clear Extension Sections Context')
+ .asyncAction(ExtensionSection.clearContext);
+
+ binding
+ .command('show-context')
+ .description('Show Extension Sections Context')
+ .asyncAction(ExtensionSection.logContext);
+
+ return binding;
+}
diff --git a/src/commands/binding/index.ts b/src/commands/binding/index.ts
new file mode 100644
index 00000000..db0211fb
--- /dev/null
+++ b/src/commands/binding/index.ts
@@ -0,0 +1,7 @@
+import { Command } from 'commander';
+
+import bindingCommandBuilder from './binding-builder';
+
+export default function env(program: Command) {
+ program.addCommand(bindingCommandBuilder());
+}
diff --git a/src/commands/index.ts b/src/commands/index.ts
index 7f362650..6a4b7c07 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -7,6 +7,7 @@ const COMMANDS = [
require('./tunnel'),
require('./extension'),
require('./partner'),
+ require('./binding'),
require('./config'),
];
diff --git a/src/helper/build.ts b/src/helper/build.ts
index 7bf8d9b3..6f63bce3 100755
--- a/src/helper/build.ts
+++ b/src/helper/build.ts
@@ -6,9 +6,45 @@ import webpack, { MultiStats } from 'webpack';
import createBaseWebpackConfig from '../helper/theme.react.config';
import fs from 'fs';
import rimraf from 'rimraf';
+import Logger from '../lib/Logger';
export const THEME_ENTRY_FILE = path.join('theme', 'index.js');
+export const VUE_THEME_ENTRY_FILE = path.join("..",'theme', 'index.js');
+export const DEV_VUE_THEME_ENTRY_FILE = path.join('theme', 'index.js');
+export const CDN_ENTRY_FILE = path.join('.fdk', 'cdn_index.js');
+
+export const dynamicCDNScript = ({assetNormalizedBasePath,vueJs }) => {
+
+ const functionSnippet = `
+ function getCDNurl() {
+ let cdnUrl = '${assetNormalizedBasePath}';
+ try {
+ if (fynd_platform_cdn) {
+ cdnUrl = fynd_platform_cdn
+ } else {
+ console.warn('Dynamic CDN path not found!');
+ }
+ } catch (error) {
+ console.error('Could not set dynamic CDN path');
+ }
+
+ return cdnUrl;
+ }
+
+ __webpack_public_path__ = getCDNurl();
+ `;
+
+ const vueSpecificBundleImport = vueJs ? `
+ import bundle from "../theme/index.js";
+ export default bundle;
+ ` : '';
+
+ return `
+ ${functionSnippet}
+
+ ${vueSpecificBundleImport}`
+}
export function build({
buildFolder,
imageCdnUrl,
@@ -23,13 +59,23 @@ export function build({
'bin',
'vue-cli-service.js',
);
+ fs.stat(CDN_ENTRY_FILE, function (err, stat) {
+ if (err == null) {
+ //deleting file if exist
+ fs.unlink(CDN_ENTRY_FILE, function (err) {
+ if (err) return console.log(err);
+ Logger.debug(' \n Existing file deleted successfully');
+ });
+ }
+ fs.appendFileSync(CDN_ENTRY_FILE, dynamicCDNScript({ assetNormalizedBasePath:(imageCdnUrl|| assetCdnUrl),vueJs: true }));
+ });
const spinner = new Spinner('Building assets using vue-cli-service');
return new Promise((resolve, reject) => {
spinner.start();
const isNodeVersionIsGreaterThan18 =
+process.version.split('.')[0].slice(1) >= 18;
let b = exec(
- `node ${VUE_CLI_PATH} build --target lib --dest ${buildFolder} --name themeBundle --filename ${assetHash}_themeBundle ${THEME_ENTRY_FILE}`,
+ `node ${VUE_CLI_PATH} build --target lib --dest ${buildFolder} --name themeBundle --filename ${assetHash}_themeBundle ${CDN_ENTRY_FILE}`,
{
cwd: process.cwd(),
env: {
@@ -53,6 +99,10 @@ export function build({
b.stdout.pipe(process.stdout);
b.stderr.pipe(process.stderr);
b.on('exit', function (code) {
+ fs.unlink(CDN_ENTRY_FILE, function (err) {
+ if (err) return console.log(err);
+ Logger.debug(' \n Existing file deleted successfully');
+ });
if (!code) {
spinner.succeed();
return resolve(true);
@@ -75,6 +125,7 @@ interface DevReactBuild {
imageCdnUrl?: string;
localThemePort?: string;
isHMREnabled: boolean;
+ targetDirectory?:string
}
export function devBuild({ buildFolder, imageCdnUrl, isProd }: DevBuild) {
@@ -91,7 +142,7 @@ export function devBuild({ buildFolder, imageCdnUrl, isProd }: DevBuild) {
return new Promise((resolve, reject) => {
let b = exec(
- `node ${VUE_CLI_PATH} build --target lib --dest ${buildFolder} --name themeBundle ${THEME_ENTRY_FILE}`,
+ `node ${VUE_CLI_PATH} build --target lib --dest ${buildFolder} --name themeBundle ${DEV_VUE_THEME_ENTRY_FILE}`,
{
cwd: process.cwd(),
env: {
@@ -125,10 +176,11 @@ export function devBuild({ buildFolder, imageCdnUrl, isProd }: DevBuild) {
export async function devReactBuild({
buildFolder,
runOnLocal,
- assetBasePath,
+ assetBasePath = '',
localThemePort,
imageCdnUrl,
isHMREnabled,
+ targetDirectory
}: DevReactBuild): Promise
{
const buildPath = path.join(process.cwd(), buildFolder);
try {
@@ -145,7 +197,6 @@ export async function devReactBuild({
themeWebpackConfigPath
));
}
-
const ctx = {
buildPath: buildPath,
NODE_ENV: (!runOnLocal && 'production') || 'development',
@@ -154,14 +205,38 @@ export async function devReactBuild({
localThemePort: localThemePort,
context: process.cwd(),
isHMREnabled,
+ targetDirectory
};
const baseWebpackConfig = createBaseWebpackConfig(
ctx,
webpackConfigFromTheme,
);
+ const assetNormalizedBasePath =
+ assetBasePath[assetBasePath.length - 1] === '/'
+ ? assetBasePath
+ : assetBasePath + '/';
return new Promise((resolve, reject) => {
+ if(!runOnLocal) {
+ fs.stat(path.resolve((targetDirectory || process.cwd()), CDN_ENTRY_FILE), function (err, stat) {
+ if (err == null) {
+ //deleting file if exist
+ fs.unlink(path.resolve((targetDirectory || process.cwd()), CDN_ENTRY_FILE), function (err) {
+ if (err) return console.log(err);
+ Logger.debug(' \n Existing file deleted successfully');
+ });
+ }
+ fs.appendFileSync(path.resolve((targetDirectory || process.cwd()), CDN_ENTRY_FILE), dynamicCDNScript({assetNormalizedBasePath, vueJs: false }));
+
+ });
+ }
webpack(baseWebpackConfig, (err, stats) => {
console.log(stats.toString());
+ if(!runOnLocal) {
+ fs.unlink(path.resolve((targetDirectory || process.cwd()), CDN_ENTRY_FILE), function (err) {
+ if (err) return console.log(err);
+ Logger.debug(' \n file deleted successfully');
+ });
+ }
if (err || stats.hasErrors()) {
reject();
}
diff --git a/src/helper/extension.react.config.ts b/src/helper/extension.react.config.ts
new file mode 100644
index 00000000..95e6e2f8
--- /dev/null
+++ b/src/helper/extension.react.config.ts
@@ -0,0 +1,142 @@
+import webpack, { Configuration } from 'webpack';
+import { mergeWithRules } from 'webpack-merge';
+
+class CustomSnippetPlugin {
+ constructor(private options) { }
+
+ apply(compiler) {
+ compiler.hooks.emit.tapAsync('CustomSnippetPlugin', (compilation, callback) => {
+ // Get the snippet code from options or use a default one
+ const snippetCode = this.options.snippetCode;
+
+ // Iterate through each asset in the compilation
+ for (const filename in compilation.assets) {
+ if (filename.endsWith('.js')) { // Only apply to JavaScript files
+ // Get the asset source
+ let source = compilation.assets[filename].source();
+
+ // Append the custom snippet code
+ source += `\n\n// Custom Snippet Start\n${snippetCode}\n// Custom Snippet End\n`;
+
+ // Update the asset with the modified source
+ compilation.assets[filename] = {
+ source: () => source,
+ size: () => source.length
+ };
+ }
+ }
+
+ callback();
+ });
+ }
+}
+
+type ExtensionBuildContext = {
+ isLocal: Boolean;
+ bundleName: string;
+ port: number
+ context: string;
+}
+
+const snippet = (port) => `
+function isRunningOnClient() {
+ if (typeof window !== 'undefined') {
+ return globalThis === window;
+ }
+
+ return false;
+}
+
+if (isRunningOnClient()) {
+ const _script = document.createElement('script');
+_script.src = "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js";
+_script.onload = function () {
+ var socket = io('http://127.0.0.1:${port}');
+ socket.on('reload', function(){
+ window.location.reload();
+ });
+};
+document.head.appendChild(_script);
+}
+`;
+
+export function extensionWebpackConfig(env: ExtensionBuildContext, webpackConfigFromBinding): Configuration[] {
+ const isLocalBuild = env.isLocal;
+
+ const extendedWebpackResolved = webpackConfigFromBinding(env);
+
+ const baseConfig: Configuration = {
+ externals: {
+ react: 'React',
+ 'react-router-dom': 'ReactRouterDOM',
+ 'fdk-core/components': 'sharedComponentLibrary',
+ 'fdk-core/utils': 'sharedUtilsLibrary',
+ 'react-helmet-async': 'helmetModule',
+ },
+ output: {
+ filename: `${env.bundleName}.umd.min.js`,
+ library: {
+ name: 'extension',
+ type: 'umd',
+ umdNamedDefine: true,
+ },
+ globalObject: 'typeof self !=="undefined" ? self : this',
+ },
+ plugins: [
+ ...(isLocalBuild ? [
+ new CustomSnippetPlugin({
+ snippetCode: snippet(env.port)
+ }),
+ ] : []),
+ ]
+ };
+
+ const sectionConfig: Configuration = {
+ mode: 'production',
+ target: 'node',
+ externals: {
+ react: 'fs',
+ 'fdk-core/components': 'fs',
+ 'react-router-dom': 'fs',
+ 'fdk-core/utils': 'fs',
+ 'react-helmet-async': 'fs',
+ },
+ output: {
+ filename: 'sections.commonjs.js',
+ // path: path.resolve(context, ""),
+ library: {
+ name: 'sections',
+ type: 'commonjs',
+ },
+ globalObject: 'typeof self !== "undefined" ? self : this',
+ },
+ plugins: [],
+ };
+
+ const mergedBaseConfig: Configuration = mergeWithRules({
+ module: {
+ rules: {
+ test: 'match',
+ use: 'append',
+ },
+ },
+ })(extendedWebpackResolved, baseConfig);
+
+ const mergedSectionConfig: Configuration = mergeWithRules({
+ module: {
+ rules: {
+ test: 'match',
+ use: 'append',
+ },
+ },
+ })(extendedWebpackResolved, sectionConfig);
+
+ if (isLocalBuild) {
+ return [mergedBaseConfig];
+ }
+
+ return [
+ mergedBaseConfig,
+ mergedSectionConfig,
+ ]
+}
diff --git a/src/helper/serve.utils.ts b/src/helper/serve.utils.ts
index e3cf6456..f4d0d3d0 100644
--- a/src/helper/serve.utils.ts
+++ b/src/helper/serve.utils.ts
@@ -13,6 +13,7 @@ import Theme from '../lib/Theme';
import glob from 'glob';
import detect from 'detect-port';
import chalk from 'chalk';
+import cors from 'cors';
import UploadService from '../lib/api/services/upload.service';
import Configstore, { CONFIG_KEYS } from '../lib/Config';
import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
@@ -23,10 +24,12 @@ import webpackHotMiddleware from 'webpack-hot-middleware';
import webpack from 'webpack';
import createBaseWebpackConfig from '../helper/theme.react.config';
import Debug from '../lib/Debug';
+import { SupportedFrameworks } from '../lib/ExtensionSection';
import https from 'https';
const packageJSON = require('../../package.json');
const BUILD_FOLDER = './.fdk/dist';
+const SERVE_BUILD_FOLDER = './.fdk/distServed';
let port = 5001;
let sockets = [];
let publicCache = {};
@@ -171,7 +174,7 @@ export async function startServer({ domain, host, isSSR, port }) {
applyProxy(app);
- app.use(express.static(path.resolve(process.cwd(), BUILD_FOLDER)));
+ app.use(express.static(path.resolve(process.cwd(), SERVE_BUILD_FOLDER)));
app.get(['/__webpack_hmr', '/manifest.json'], async (req, res, next) => {
return res.end();
});
@@ -193,7 +196,7 @@ export async function startServer({ domain, host, isSSR, port }) {
const BUNDLE_PATH = path.join(
process.cwd(),
- path.join('.fdk', 'dist', 'themeBundle.common.js'),
+ path.join('.fdk', 'distServed', 'themeBundle.common.js'),
);
if (!fs.existsSync(BUNDLE_PATH))
return res.sendFile(
@@ -209,7 +212,7 @@ export async function startServer({ domain, host, isSSR, port }) {
if (isSSR) {
const BUNDLE_PATH = path.join(
process.cwd(),
- '/.fdk/dist/themeBundle.common.js',
+ '/.fdk/distServed/themeBundle.common.js',
);
const User = Configstore.get(CONFIG_KEYS.AUTH_TOKEN);
themeUrl = (
@@ -218,7 +221,7 @@ export async function startServer({ domain, host, isSSR, port }) {
'fdk-cli-dev-files',
User.current_user._id,
)
- ).start.cdn.url;
+ ).complete.cdn.url;
} else {
jetfireUrl.searchParams.set('__csr', 'true');
}
@@ -262,24 +265,24 @@ export async function startServer({ domain, host, isSSR, port }) {
)}">`,
);
const umdJsAssests = glob
- .sync(`${Theme.BUILD_FOLDER}/themeBundle.umd.**.js`)
+ .sync(`${Theme.SERVE_BUILD_FOLDER}/themeBundle.umd.**.js`)
.filter((x) => !x.includes('.min.'));
umdJsAssests.forEach((umdJsLink) => {
umdJsInitial.after(
``,
);
});
- const cssAssests = glob.sync(`${Theme.BUILD_FOLDER}/**.css`);
+ const cssAssests = glob.sync(`${Theme.SERVE_BUILD_FOLDER}/**.css`);
const cssInitial = $('link[data-css-cli-source="initial"]');
cssAssests.forEach((cssLink) => {
cssInitial.after(
` `,
);
});
@@ -296,7 +299,7 @@ export async function startServer({ domain, host, isSSR, port }) {
errorString = `${errorString} `;
const mapContent = JSON.parse(
fs.readFileSync(
- `${BUILD_FOLDER}/themeBundle.common.js.map`,
+ `${SERVE_BUILD_FOLDER}/themeBundle.common.js.map`,
{ encoding: 'utf8', flag: 'r' },
),
);
@@ -305,20 +308,17 @@ export async function startServer({ domain, host, isSSR, port }) {
stack?.forEach(({ methodName, lineNumber, column }) => {
try {
if (lineNumber == null || lineNumber < 1) {
- errorString += ` at ${
- methodName || ''
- }
`;
+ errorString += ` at ${methodName || ''
+ }
`;
} else {
const pos = smc.originalPositionFor({
line: lineNumber,
column,
});
if (pos && pos.line != null) {
- errorString += ` at ${
- methodName || pos.name || ''
- } (${pos.source}:${pos.line}:${
- pos.column
- })
`;
+ errorString += ` at ${methodName || pos.name || ''
+ } (${pos.source}:${pos.line}:${pos.column
+ })
`;
}
}
} catch (err) {
@@ -343,8 +343,7 @@ export async function startServer({ domain, host, isSSR, port }) {
return reject(err);
}
Logger.info(
- `Starting starter at port -- ${port} in ${
- isSSR ? 'SSR' : 'Non-SSR'
+ `Starting starter at port -- ${port} in ${isSSR ? 'SSR' : 'Non-SSR'
} mode`,
);
Logger.info(`************* Using Debugging build`);
@@ -370,7 +369,7 @@ export async function startReactServer({ domain, host, isHMREnabled, port }) {
}
const ctx = {
- buildPath: path.resolve(process.cwd(), Theme.BUILD_FOLDER),
+ buildPath: path.resolve(process.cwd(), Theme.SERVE_BUILD_FOLDER),
NODE_ENV: 'development',
localThemePort: port,
context: process.cwd(),
@@ -394,7 +393,7 @@ export async function startReactServer({ domain, host, isHMREnabled, port }) {
app.use(webpackHotMiddleware(compiler));
}
- app.use(express.static(path.resolve(process.cwd(), BUILD_FOLDER)));
+ app.use(express.static(path.resolve(process.cwd(), SERVE_BUILD_FOLDER)));
app.use((request, response, next) => {
// Filtering so that HMR file requests are not routed to skyfire pods
@@ -429,7 +428,7 @@ export async function startReactServer({ domain, host, isHMREnabled, port }) {
currentContext.theme_id,
);
}
- const BUNDLE_DIR = path.join(process.cwd(), path.join('.fdk', 'dist'));
+ const BUNDLE_DIR = path.join(process.cwd(), path.join('.fdk', 'distServed'));
if (req.originalUrl == '/favicon.ico' || req.originalUrl == '/.webp') {
return res.status(404).send('Not found');
}
@@ -498,9 +497,8 @@ export async function startReactServer({ domain, host, isHMREnabled, port }) {
@@ -535,3 +533,40 @@ export async function startReactServer({ domain, host, isHMREnabled, port }) {
});
});
}
+
+type ExtensionServerOptions = {
+ bundleDist: string;
+ port: number;
+ framework: SupportedFrameworks;
+};
+export async function startExtensionServer(options: ExtensionServerOptions) {
+ const { bundleDist, port, framework } = options;
+ const app = express();
+ const server = require('http').createServer(app);
+
+ if (framework === 'react') {
+ const io = require('socket.io')(server);
+
+ io.on('connection', function (socket) {
+ sockets.push(socket);
+ socket.on('disconnect', function () {
+ sockets = sockets.filter((s) => s !== socket);
+ });
+ });
+ }
+ app.use(cors());
+ // parse application/x-www-form-urlencoded
+ app.use(express.json());
+
+ app.use(express.static(bundleDist));
+
+ return new Promise((resolve, reject) => {
+ server.listen(port, (err) => {
+ if (err) {
+ return reject(err);
+ }
+ Logger.info(`Starting server at port -- ${port}`);
+ resolve(true);
+ });
+ });
+}
diff --git a/src/helper/theme.react.config.ts b/src/helper/theme.react.config.ts
index 060b44fc..b7c09679 100644
--- a/src/helper/theme.react.config.ts
+++ b/src/helper/theme.react.config.ts
@@ -1,8 +1,11 @@
+import path from "path"
import TerserPlugin from 'terser-webpack-plugin';
import webpack, { Configuration } from 'webpack';
import { mergeWithRules, merge } from 'webpack-merge';
import { getLocalBaseUrl } from './serve.utils';
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
+import { CDN_ENTRY_FILE } from './build';
+const context = process.cwd();
const baseConfig = (configOptions) => {
const {
@@ -15,7 +18,6 @@ const baseConfig = (configOptions) => {
return {
mode: isLocal ? 'development' : 'production',
-
devtool: isLocal ? 'source-map' : false,
optimization: {
minimizer: [
@@ -83,6 +85,7 @@ export default (ctx, extendedWebpackConfig): Configuration[] => {
imageCdnUrl = '',
localThemePort = 5500,
isHMREnabled = true,
+ targetDirectory
} = ctx;
const assetNormalizedBasePath =
@@ -109,7 +112,6 @@ export default (ctx, extendedWebpackConfig): Configuration[] => {
};
const baseWebpackConfig = baseConfig(configOptions);
const extendedWebpackResolved = extendedWebpackConfig(configOptions);
-
const mergedBaseConfig: Configuration = mergeWithRules({
module: {
rules: {
@@ -120,13 +122,13 @@ export default (ctx, extendedWebpackConfig): Configuration[] => {
})(extendedWebpackResolved, baseWebpackConfig);
if (mergedBaseConfig.entry.hasOwnProperty('themeBundle')) {
- mergedBaseConfig.entry['themeBundle'] =
- isLocal && isHMREnabled
- ? [
- require.resolve('webpack-hot-middleware/client'),
- ...mergedBaseConfig.entry['themeBundle'],
- ]
- : mergedBaseConfig.entry['themeBundle'];
+ let entryPoints = [...mergedBaseConfig.entry['themeBundle']];
+ if (isLocal && isHMREnabled) {
+ entryPoints.unshift(require.resolve('webpack-hot-middleware/client'));
+ } else if (!isLocal) {
+ entryPoints.unshift(path.resolve(targetDirectory || context, CDN_ENTRY_FILE));
+ }
+ mergedBaseConfig.entry['themeBundle'] = entryPoints;
}
return [mergedBaseConfig];
diff --git a/src/lib/Auth.ts b/src/lib/Auth.ts
index 5369bef2..eea9b363 100644
--- a/src/lib/Auth.ts
+++ b/src/lib/Auth.ts
@@ -1,5 +1,6 @@
import CommandError from './CommandError';
import Logger from './Logger';
+import chalk from 'chalk';
import inquirer from 'inquirer';
import ConfigStore, { CONFIG_KEYS } from './Config';
import open from 'open';
@@ -14,7 +15,6 @@ const SERVER_TIMER = 1000 * 60 * 2; // 2 min
import { OutputFormatter, successBox } from '../helper/formatter';
import OrganizationService from './api/services/organization.service';
import { getOrganizationDisplayName } from '../helper/utils';
-import chalk from 'chalk';
async function checkTokenExpired(auth_token) {
const { expiry_time } = auth_token;
@@ -80,7 +80,7 @@ function startTimer(){
}
function resetTimer(){
- if (Auth.timer_id) {
+ if (Auth.timer_id) {
Debug("Server timer stoped")
clearTimeout(Auth.timer_id)
Auth.timer_id = null;
@@ -127,24 +127,24 @@ export default class Auth {
public static async login(options) {
let env: string;
-
+
if(options.host){
env = await Env.verifyAndSanitizeEnvValue(options.host);
}
else{
env = 'api.fynd.com';
}
-
+
let current_env = Env.getEnvValue();
if(current_env !== env){
// update new domain after login
Auth.newDomainToUpdate = env;
-
+
// Logout user from current domain
Auth.updateConfigStoreForLogout();
}
-
+
const isLoggedIn = await Auth.isAlreadyLoggedIn();
if (isLoggedIn) {
Logger.info(
@@ -168,7 +168,7 @@ export default class Auth {
await startServer();
}
});
- } else
+ } else
await startServer();
try {
let domain = null;
diff --git a/src/lib/ExtensionSection.ts b/src/lib/ExtensionSection.ts
new file mode 100644
index 00000000..86838847
--- /dev/null
+++ b/src/lib/ExtensionSection.ts
@@ -0,0 +1,1008 @@
+import CommandError from './CommandError';
+import path from 'path';
+import fs from 'node:fs';
+import detect from 'detect-port';
+import fsExtra from 'fs-extra';
+import { promisify } from 'node:util';
+import { tunnel as startCloudflareTunnel, bin, install } from 'cloudflared';
+
+import Logger from './Logger';
+import Spinner from '../helper/spinner';
+import { installNpmPackages } from '../helper/utils';
+import { readFile } from '../helper/file.utils';
+import { extensionWebpackConfig } from '../helper/extension.react.config';
+import { webpack } from 'webpack';
+import uploadService from './api/services/upload.service';
+import Configstore, { CONFIG_KEYS } from './Config';
+import extensionService from './api/services/extension.service';
+import {
+ startExtensionServer,
+ reload,
+ getPort,
+} from '../helper/serve.utils';
+import chalk from 'chalk';
+import Theme from './Theme';
+import configurationService from './api/services/configuration.service';
+import inquirer from 'inquirer';
+import { exec } from 'child_process';
+import * as cheerio from 'cheerio';
+import chokidar from 'chokidar';
+import { v4 as uuidv4 } from 'uuid';
+import themeService from './api/services/theme.service';
+import { getPlatformUrls } from './api/services/url';
+import Tunnel from './Tunnel';
+
+const readDirectories = promisify(fs.readdir);
+
+type BindingInterface = 'Web Theme' | 'Platform';
+
+export type SupportedFrameworks = 'react' | 'vue2';
+
+type AppliedThemeData = {
+ applicationId: string;
+ companyId: string;
+ themeId: string;
+ companyType: 'live' | 'development';
+};
+
+type ContextData = {
+ organisationId: string;
+ extensionId: string;
+ name: string;
+ framework: SupportedFrameworks;
+ interface: BindingInterface;
+ appliedTheme: AppliedThemeData;
+ url?: string;
+ port?: number;
+};
+
+type ExtensionSectionOptions = {
+ name: string;
+ interface: BindingInterface;
+ framework: SupportedFrameworks;
+};
+
+interface SyncExtensionBindingsOptions extends ContextData { };
+
+type ExtensionContext = {
+ extensionId: string;
+ organisationId: string;
+ domain: string;
+ interface: string;
+};
+
+type CommandType = 'init' | 'draft' | 'publish' | 'preview';
+
+export default class ExtensionSection {
+ static BINDINGS_DIR_REACT = 'bindings/theme/react';
+ static BINDINGS_DIR_VUE = 'bindings/theme/vue';
+ static CONTEXT_FILENAME = 'context.json';
+ static CONTEXT_DIR_PATH = '.fdk';
+
+ static async getContextData(
+ optionsPassed: any,
+ commandType: CommandType,
+ ): Promise {
+ const commonRequiredOptions = [
+ 'extensionId',
+ 'organisationId',
+ 'name',
+ 'framework',
+ 'interface',
+ ] as const;
+ const allOptions = [
+ ...commonRequiredOptions,
+ 'port',
+ 'url',
+ 'appliedTheme',
+ ] as const;
+
+ type AllOptions = (typeof allOptions)[number];
+
+ type PromptUserOption = {
+ type: string;
+ name: string;
+ message: string;
+ choices?: string[];
+ };
+
+ async function promptUser(options: PromptUserOption) {
+ const questions = [options];
+
+ const answers = await inquirer.prompt(questions);
+
+ return answers[options.name];
+ }
+
+ async function getOption(key: (typeof allOptions)[number]) {
+ try {
+ switch (key) {
+ case 'framework':
+ return promptUser({
+ type: 'list',
+ name: 'framework',
+ message: 'Please select your framework: ',
+ choices: ['react', 'vue2'],
+ });
+
+ case 'extensionId':
+ let extensionId;
+ try {
+ const extensionsList =
+ await extensionService.getExtensionList(1, 500);
+
+ const extensions = extensionsList?.items.map(
+ ({ name }) => name,
+ );
+
+ if (!extensions?.length) {
+ throw new Error(
+ 'No installed extensions found!',
+ );
+ }
+ const selectedExtensionName = await promptUser({
+ type: 'list',
+ name: 'extensionId',
+ message: 'Please select your extension: ',
+ choices: extensions,
+ });
+ extensionId = extensionsList?.items.find(
+ ({ name }) => name === selectedExtensionName,
+ )?._id;
+ } catch (error) {
+ Logger.error(
+ 'Could not fetch the list of extensions',
+ );
+ extensionId = await promptUser({
+ type: 'text',
+ name: 'extensionId',
+ message: 'Please Enter your extensionId: ',
+ });
+ } finally {
+ Configstore.set(
+ 'extensionSections.extensionId',
+ extensionId,
+ );
+ return extensionId;
+ }
+
+ case 'name':
+ return promptUser({
+ type: 'text',
+ name: 'name',
+ message: 'Please enter your binding name: ',
+ });
+
+ case 'port':
+ return promptUser({
+ type: 'text',
+ name: 'port',
+ message: 'Please enter server port: ',
+ });
+
+ case 'url':
+ return promptUser({
+ type: 'text',
+ name: 'url',
+ message: 'Please enter tunnel url: ',
+ });
+
+ case 'interface':
+ return promptUser({
+ type: 'list',
+ name: 'interface',
+ message: 'Please select your extension interface: ',
+ choices: ['Web Theme', 'Platform', 'Store OS'],
+ });
+
+ case 'appliedTheme':
+ let themeDetails = {
+ applicationId: undefined,
+ companyId: undefined,
+ themeId: undefined,
+ companyType: 'live',
+ };
+ try {
+ const configObj =
+ await Theme.selectCompanyAndStore();
+ const { data: appConfig } =
+ await configurationService.getApplicationDetails(
+ configObj,
+ );
+
+ const themeData =
+ await themeService.getAppliedTheme({
+ company_id: appConfig.company_id,
+ application_id: appConfig.id,
+ });
+
+ themeDetails = {
+ applicationId: appConfig['id'],
+ companyId: appConfig['company_id'],
+ themeId: themeData['_id'],
+ companyType: configObj['accountType'],
+ };
+ } catch (error) {
+ Logger.error('Could not fetch the applied!');
+ for (let lkey in themeDetails) {
+ themeDetails[lkey] = await promptUser({
+ type: 'text',
+ name: lkey,
+ message: `Please enter ${lkey}: `,
+ });
+ }
+ } finally {
+ Configstore.set(
+ 'extensionSections.appliedTheme',
+ themeDetails,
+ );
+ return themeDetails;
+ }
+
+ default:
+ return null;
+ }
+ } catch (error) { }
+ }
+
+ if (!Configstore.all.extensionSections) {
+ Configstore.set('extensionSections', {});
+ }
+
+ Configstore.set(
+ 'extensionSections.organisationId',
+ Configstore.all.current_env.organization,
+ );
+
+ const requiredKeys: {
+ [key in CommandType]: ReadonlyArray;
+ } = {
+ init: [...commonRequiredOptions],
+ draft: [...commonRequiredOptions],
+ publish: [...commonRequiredOptions],
+ preview: [...commonRequiredOptions, 'appliedTheme'],
+ };
+ try {
+ const requiredOptions = requiredKeys[commandType];
+ const existingContext = Configstore.all.extensionSections;
+
+ const mergedConfig = Object.assign(
+ {},
+ existingContext,
+ optionsPassed,
+ );
+
+ const missingKeys = requiredOptions.filter(
+ (key) =>
+ !Object.prototype.hasOwnProperty.call(mergedConfig, key),
+ );
+
+ const userInput = {};
+ for (let val in missingKeys) {
+ const result = await getOption(missingKeys[val]);
+ userInput[missingKeys[val]] = result;
+ }
+
+ const finalContext = Object.assign(mergedConfig, userInput);
+
+ return finalContext;
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ public static clearContext() {
+ Configstore.set('extensionSections', {});
+ }
+
+ static async startTunnel() {
+
+ try {
+ const port = await getPort(5500);
+
+ const tunnelInstance = new Tunnel({
+ port,
+ })
+
+ const tunnelUrl = await tunnelInstance.startTunnel();
+
+ console.log(`
+ Started cloudflare tunnel at ${port}: ${tunnelUrl}`)
+ return {
+ url: tunnelUrl,
+ port,
+ };
+ } catch (error) {
+ Logger.error('Error during starting cloudflare tunnel: ' + error.message);
+ return;
+ }
+ }
+
+ public static logContext() {
+ console.table(Configstore.get('extensionSections'));
+ }
+
+ public static async initExtensionBinding(options: ExtensionSectionOptions) {
+ try {
+ const context = await ExtensionSection.getContextData(
+ options,
+ 'init',
+ );
+
+ const { interface: bindingInterface, framework, name: bindingName } = context;
+
+ if (bindingInterface === 'Web Theme') {
+
+ if (!bindingName) {
+ throw new Error('Section Name not provided!');
+ }
+
+ ExtensionSection.createSectionsDirectoryIfNotExists(framework);
+
+ const sectionExists = await ExtensionSection.sectionExists(
+ bindingName,
+ framework,
+ );
+
+ if (sectionExists) {
+ throw new Error('Section Already Exists!');
+ }
+
+ if (framework === 'react' || framework === 'vue2') {
+
+ const isReact = framework === 'react';
+ const sourcePath = path.resolve(
+ __dirname,
+ isReact ? '../../extension-section' : '../../extension-section-vue',
+ );
+
+ const destinationPath = path.resolve(
+ process.cwd(),
+ isReact ? ExtensionSection.BINDINGS_DIR_REACT : ExtensionSection.BINDINGS_DIR_VUE,
+ bindingName,
+ )
+
+ await fsExtra.copy(sourcePath, destinationPath);
+
+ process.chdir(
+ path.join(destinationPath),
+ );
+
+ await ExtensionSection.installNpmPackages();
+ Logger.info('Binding created with default sections!')
+ } else {
+ throw new CommandError('Unsupported framework!');
+ }
+ }
+
+
+ } catch (error) {
+ throw new CommandError(error.message, error.code);
+ }
+ }
+
+ static createSectionsDirectoryIfNotExists(framework: string): void {
+ const directories =
+ framework === 'react'
+ ? ExtensionSection.BINDINGS_DIR_REACT.split(path.sep)
+ : ExtensionSection.BINDINGS_DIR_VUE.split(path.sep);
+ let currentPath = process.cwd();
+
+ directories.forEach((directory) => {
+ currentPath = path.join(currentPath, directory);
+ if (!fs.existsSync(currentPath)) {
+ fs.mkdirSync(currentPath);
+ }
+ });
+ }
+
+ static async sectionExists(
+ name: string,
+ framework: string,
+ ): Promise {
+ const sectionPath = path.resolve(
+ process.cwd(),
+ framework === 'react'
+ ? ExtensionSection.BINDINGS_DIR_REACT
+ : ExtensionSection.BINDINGS_DIR_VUE,
+ );
+
+ if (!fs.existsSync(sectionPath)) {
+ return false;
+ }
+
+ const dirents = await readDirectories(sectionPath);
+
+ const sectionExists = dirents.some((dirent) => {
+ const direntFullPath = path.resolve(sectionPath, dirent);
+ return fs.statSync(direntFullPath).isDirectory() && dirent === name;
+ });
+
+ return sectionExists;
+ }
+
+ static isValidSyncOptions(options: SyncExtensionBindingsOptions): Boolean {
+ return (
+ options.extensionId &&
+ options.organisationId &&
+ options.name &&
+ options.framework &&
+ typeof options.extensionId === 'string' &&
+ typeof options.organisationId === 'string' &&
+ typeof options.name === 'string' &&
+ typeof options.framework === 'string'
+ );
+ }
+
+ public static async publishExtensionBindings(
+ options: SyncExtensionBindingsOptions,
+ ) {
+ const context = await ExtensionSection.getContextData(
+ options,
+ 'publish',
+ );
+ const { interface: bindingInterface, framework, name } = context;
+
+ if (bindingInterface === 'Web Theme') {
+ if (framework === 'react' || framework === 'vue2') {
+ Logger.info(`Publishing Extension Bindings`);
+
+ const sectionData = await ExtensionSection.buildAndExtractSections(
+ context,
+ );
+ sectionData.status = 'published';
+
+ await ExtensionSection.savingExtensionBindings(
+ sectionData,
+ context,
+ sectionData.status,
+ );
+ } else {
+ throw new CommandError(
+ 'Unsupported Framework! Only react and vue2 are supported',
+ );
+ }
+ } else {
+ throw new CommandError(
+ 'Unsupported Interface! Only Web Themes are supported',
+ );
+ }
+
+ Logger.info('Code published ...');
+ }
+
+ static async buildExtensionCodeVue({ bundleName }) {
+ const VUE_CLI_PATH = path.join(
+ '.',
+ 'node_modules',
+ '@vue',
+ 'cli-service',
+ 'bin',
+ 'vue-cli-service.js',
+ );
+ const spinner = new Spinner('Building sections using vue-cli-service');
+ return new Promise((resolve, reject) => {
+ spinner.start();
+ const isNodeVersionIsGreaterThan18 =
+ +process.version.split('.')[0].slice(1) >= 18;
+ let b = exec(
+ `node ${VUE_CLI_PATH} build --target lib src/index.js --name ${bundleName}`,
+ {
+ cwd: process.cwd(),
+ env: {
+ ...process.env,
+ NODE_ENV: 'production',
+ VUE_CLI_SERVICE_CONFIG_PATH: path.join(
+ process.cwd(),
+ Theme.VUE_CLI_CONFIG_PATH,
+ ),
+ ...(isNodeVersionIsGreaterThan18 && {
+ NODE_OPTIONS: '--openssl-legacy-provider',
+ }),
+ },
+ },
+ );
+
+ b.stdout.pipe(process.stdout);
+ b.stderr.pipe(process.stderr);
+ b.on('exit', function (code) {
+ if (!code) {
+ spinner.succeed();
+ return resolve(true);
+ }
+ spinner.fail();
+ reject({ message: 'Extension Build Failed' });
+ });
+ });
+ }
+
+ static extractSettingsFromFile(path) {
+ try {
+ let $ = cheerio.load(readFile(path));
+ let settingsText = $('settings').text();
+
+ try {
+ return settingsText ? JSON.parse(settingsText) : {};
+ } catch (err) {
+ throw new Error(
+ `Invalid settings JSON object in ${path}. Validate JSON from https://jsonlint.com/`,
+ );
+ }
+ } catch (error) {
+ throw new Error(
+ `Invalid settings JSON object in ${path}. Validate JSON from https://jsonlint.com/`,
+ );
+ }
+ }
+
+ static async buildAndExtractSections(
+ context: SyncExtensionBindingsOptions,
+ ): Promise {
+ const { name: bundleName, framework } = context;
+ const isReact = framework === 'react';
+ const currentRoot = process.cwd();
+
+ const destinationPath = path.join(
+ currentRoot,
+ isReact ? ExtensionSection.BINDINGS_DIR_REACT : ExtensionSection.BINDINGS_DIR_VUE,
+ bundleName,
+ )
+
+ process.chdir(destinationPath);
+
+ if (isReact) {
+ await ExtensionSection.buildExtensionCode({ bundleName }).catch(
+ console.error,
+ );
+ } else {
+ await ExtensionSection.buildExtensionCodeVue({
+ bundleName: context.name,
+ }).catch(console.error);
+ }
+
+ const uploadURLs = await ExtensionSection.uploadSectionFiles(bundleName);
+
+ let sections = [];
+
+ if (isReact) {
+ const availableSections = await ExtensionSection.getAvailableSections();
+
+ for (const key in availableSections) {
+ if (availableSections.hasOwnProperty(key)) {
+ sections.push(availableSections[key]['settings']);
+ }
+ }
+ } else {
+ try {
+ const sectionsFiles = fs
+ .readdirSync(
+ path.join(
+ currentRoot,
+ `/${ExtensionSection.BINDINGS_DIR_VUE}/${context.name}/src/sections`,
+ ),
+ )
+ .filter((o) => o != 'index.js');
+ sections = sectionsFiles.map((f) => {
+ return ExtensionSection.extractSettingsFromFile(
+ path.join(
+ currentRoot,
+ `/${ExtensionSection.BINDINGS_DIR_VUE}/${context.name}/src/sections/${f}`,
+ ),
+ );
+ });
+ } catch (err) {
+ console.log(err);
+ }
+
+ }
+
+ const data = {
+ extension_id: context.extensionId,
+ bundle_name: bundleName,
+ organization_id: context.organisationId,
+ sections,
+ assets: uploadURLs,
+ type: framework,
+ };
+
+ process.chdir(currentRoot);
+ return data;
+ }
+
+ static async buildExtensionCode({
+ bundleName,
+ isLocal = false,
+ port = 5502,
+ }): Promise<{ jsFile: string; cssFile: string }> {
+ let spinner = new Spinner('Building Extension Code');
+ try {
+ spinner.start();
+ const context = process.cwd();
+ let webpackConfigFromBinding = {};
+ const webpackExtendedPath = path.join(
+ context,
+ 'webpack.config.js'
+ );
+
+ if (fs.existsSync(webpackExtendedPath)) {
+ ({ default: webpackConfigFromBinding } = await import(
+ webpackExtendedPath
+ ));
+ }
+
+ const webpackConfig = extensionWebpackConfig({
+ isLocal,
+ bundleName,
+ port,
+ context,
+ }, webpackConfigFromBinding);
+
+ return new Promise((resolve, reject) => {
+ webpack(webpackConfig, (err, stats) => {
+ console.log(stats.toString());
+ if (err || stats.hasErrors()) {
+ reject();
+ }
+ spinner.succeed();
+ const jsFile =
+ stats.stats[0].compilation.outputOptions.filename.toString();
+ const cssFile =
+ stats.stats[0].compilation.outputOptions.cssFilename.toString();
+ resolve({ jsFile, cssFile });
+ });
+ });
+ } catch (error) {
+ spinner.fail();
+ throw new CommandError(error.message);
+ }
+ }
+
+ public static async draftExtensionBindings(
+ options: SyncExtensionBindingsOptions,
+ ) {
+ const context = await ExtensionSection.getContextData(options, 'draft');
+
+ const { interface: bindingInterface, framework, name } = context;
+
+ if (bindingInterface === 'Web Theme') {
+ if (framework === 'react' || framework === 'vue2') {
+ Logger.info(`Creating drafts for Extension Bindings`);
+
+ const sectionData = await ExtensionSection.buildAndExtractSections(
+ context,
+ );
+ sectionData.status = 'draft';
+
+ await ExtensionSection.savingExtensionBindings(
+ sectionData,
+ context,
+ sectionData.status,
+ );
+ } else {
+ throw new CommandError(
+ 'Unsupported Framework! Only react and vue2 are supported',
+ );
+ }
+ } else {
+ throw new CommandError(
+ 'Unsupported Interface! Only Web Themes are supported',
+ );
+ }
+
+ Logger.info('Draft successful!');
+ }
+
+ static async watchExtensionCodeBuild(
+ bundleName: string,
+ port: number = 5502,
+ callback: Function,
+ ) {
+ let spinner = new Spinner('Building Extension Code');
+ try {
+ spinner.start();
+
+ const context = process.cwd();
+
+ let webpackConfigFromBinding = {};
+ const webpackExtendedPath = path.join(
+ context,
+ 'webpack.config.js'
+ );
+
+ if (fs.existsSync(webpackExtendedPath)) {
+ ({ default: webpackConfigFromBinding } = await import(
+ webpackExtendedPath
+ ));
+ }
+
+ const webpackConfig = extensionWebpackConfig({
+ isLocal: true,
+ bundleName,
+ port,
+ context,
+ }, webpackConfigFromBinding);
+
+ const compiler = webpack(webpackConfig);
+
+ compiler.watch(
+ {
+ aggregateTimeout: 1500,
+ ignored: /node_modules/,
+ poll: undefined,
+ },
+ (err, stats) => {
+ if (err || stats.hasErrors()) {
+ console.log(stats.toString());
+ throw err;
+ }
+ callback(stats);
+ },
+ );
+ } catch (error) {
+ spinner.fail();
+ throw new CommandError(error.message);
+ }
+ }
+
+ static async installNpmPackages() {
+ let spinner = new Spinner('Installing npm packages');
+ try {
+ spinner.start();
+ await installNpmPackages();
+ spinner.succeed();
+ } catch (error) {
+ spinner.fail();
+ throw new CommandError(error.message);
+ }
+ }
+
+ static async uploadSectionFiles(sectionName: string) {
+ const BUNDLE_DIR = path.join(process.cwd(), path.join('dist'));
+ const User = Configstore.get(CONFIG_KEYS.AUTH_TOKEN);
+
+ let isReact = process
+ .cwd()
+ .includes(ExtensionSection.BINDINGS_DIR_REACT);
+ let files;
+ if (!isReact) {
+ files = [
+ ['js', `${sectionName}.umd.min.js`],
+ ['css', `${sectionName}.css`],
+ ];
+ } else {
+ files = [
+ ['js', `${sectionName}.umd.min.js`],
+ ['css', `${sectionName}.umd.min.css`],
+ ];
+ }
+
+ const uploadURLs = {};
+ const promises = files.map(([fileExtension, fileName]) => {
+ return uploadService
+ .uploadFile(
+ path.join(BUNDLE_DIR, fileName),
+ 'fdk-cli-dev-files',
+ User.current_user._id,
+ )
+ .then((response) => {
+ const url = response.complete.cdn.url;
+ uploadURLs[fileExtension] = url;
+ });
+ });
+
+ await Promise.all(promises);
+
+ return uploadURLs;
+ }
+
+ static async getAvailableSections() {
+ const BUNDLE_DIR = path.join(process.cwd(), path.join('dist'));
+
+ const sectionFileName = 'sections.commonjs.js';
+ const sectionFilePath = path.resolve(BUNDLE_DIR, sectionFileName);
+
+ let sectionsMeta = undefined;
+
+ try {
+ sectionsMeta = require(sectionFilePath);
+ } catch (error) {
+ throw new Error(
+ 'Cannot read section data from bundled file : ',
+ error.message,
+ );
+ }
+
+ return sectionsMeta?.sections?.default ?? {};
+ }
+
+ static async savingExtensionBindings(
+ data: any,
+ context: SyncExtensionBindingsOptions,
+ type: 'draft' | 'published' = 'draft',
+ ) {
+ try {
+ const functionMap = {
+ draft: 'draftExtensionBindings',
+ published: 'publishExtensionBindings',
+ };
+ await extensionService[functionMap[type]](
+ context.extensionId,
+ context.organisationId,
+ data,
+ );
+ } catch (error) {
+ console.log(error);
+ }
+ }
+ public static async previewExtension(options: any) {
+ const context = await ExtensionSection.getContextData(
+ options,
+ 'preview',
+ );
+
+ const { interface: bindingInterface, framework } = context;
+
+ if (bindingInterface === 'Web Theme') {
+ if (framework === 'react' || framework === 'vue2') {
+ Logger.info(`Previewing Extension Binding`);
+ try {
+ const {
+ name: bundleName,
+ appliedTheme,
+ extensionId,
+ organisationId,
+ framework,
+ } = context;
+
+ const { port, url: tunnelUrl } = await ExtensionSection.startTunnel();
+
+ const {
+ companyId,
+ applicationId,
+ themeId
+ } = appliedTheme;
+
+ const isReact = framework === 'react';
+
+ const { _id: extensionSectionId } =
+ await extensionService.getExtensionBindings(
+ extensionId,
+ organisationId,
+ bundleName,
+ appliedTheme.companyType,
+ framework
+ );
+
+ const { platform } = getPlatformUrls();
+
+ const domain = `${platform}/company/${companyId}/application/${applicationId}/themes/${themeId}/edit`;
+
+ const rootPath = process.cwd();
+
+ const destinationPath = path.join(
+ process.cwd(),
+ isReact ? ExtensionSection.BINDINGS_DIR_REACT : ExtensionSection.BINDINGS_DIR_VUE,
+ bundleName,
+ )
+ process.chdir(destinationPath);
+
+ let data;
+
+ Logger.info('Building Extension Code ...');
+
+ if (isReact) {
+ const { jsFile, cssFile } =
+ await ExtensionSection.buildExtensionCode({
+ bundleName,
+ port,
+ isLocal: true,
+ });
+ ExtensionSection.watchExtensionCodeBuild(
+ bundleName,
+ port,
+ (stats) => {
+ reload();
+ },
+ );
+ const bundleDist = path.resolve(
+ rootPath,
+ ExtensionSection.BINDINGS_DIR_REACT,
+ bundleName,
+ 'dist',
+ );
+ Logger.info('Starting Local Extension Server ...', bundleDist);
+ await startExtensionServer({ bundleDist, port, framework });
+
+ const assetUrls = {
+ js: `${tunnelUrl}/${jsFile}`,
+ css: `${tunnelUrl}/${cssFile}`,
+ };
+
+ data = {
+ id: extensionSectionId,
+ assets: assetUrls,
+ };
+
+ } else {
+ const res = await ExtensionSection.buildExtensionCodeVue({
+ bundleName,
+ });
+
+ let watcher = chokidar.watch(path.resolve(process.cwd(), 'src'), {
+ persistent: true,
+ });
+ watcher.on('change', async () => {
+ Logger.info(chalk.bold.green(`building`));
+ await ExtensionSection.buildExtensionCodeVue({
+ bundleName,
+ });
+ });
+
+ const bundleDist = path.resolve(
+ rootPath,
+ ExtensionSection.BINDINGS_DIR_VUE,
+ bundleName,
+ 'dist',
+ );
+ Logger.info('Starting Local Extension Server ...', bundleDist);
+ await startExtensionServer({ bundleDist, port, framework });
+
+ const assetUrls = {
+ js: `${tunnelUrl}/${bundleName}.umd.js`,
+ css: `${tunnelUrl}/${bundleName}.css`,
+ };
+
+ data = {
+ id: extensionSectionId,
+ assets: assetUrls,
+ };
+ }
+
+ const sectionPreviewHash = await uuidv4();
+
+ const urls = await extensionService.previewExtensionBindings(
+ extensionSectionId,
+ organisationId,
+ {
+ application_id: applicationId,
+ sectionPreviewHash,
+ ...data,
+ }
+ );
+
+ const previewURL = `${domain}?section_preview_hash=${sectionPreviewHash}`;
+
+ console.log(`PREVIEW URL :\n\n ${previewURL}\n\n`);
+
+
+ // Register a process termination listener here
+ process.on('SIGINT', async function deleteRedisSession() {
+ const data = await extensionService.deleteExtensionBindings(
+ extensionSectionId,
+ organisationId,
+ {
+ application_id: applicationId,
+ sectionPreviewHash,
+ }
+ );
+ Logger.info('Preview Session Closed');
+ process.exit(0);
+ })
+ } catch (error) {
+ Logger.error(error);
+ }
+ } else {
+ throw new CommandError(
+ 'Unsupported Framework! Only react and vue2 are supported',
+ );
+ }
+ } else {
+ throw new CommandError(
+ 'Unsupported Interface! Only Web Themes are supported',
+ );
+ }
+
+
+ }
+
+}
diff --git a/src/lib/Theme.ts b/src/lib/Theme.ts
index 67cb81e2..2aa50d2d 100644
--- a/src/lib/Theme.ts
+++ b/src/lib/Theme.ts
@@ -34,7 +34,7 @@ import ThemeService from './api/services/theme.service';
import UploadService from './api/services/upload.service';
import ExtensionService from './api/services/extension.service';
import {
- THEME_ENTRY_FILE,
+ DEV_VUE_THEME_ENTRY_FILE,
build,
devBuild,
devReactBuild,
@@ -87,6 +87,7 @@ export default class Theme {
'react-template',
);
static BUILD_FOLDER = './.fdk/dist';
+ static SERVE_BUILD_FOLDER = './.fdk/distServed';
static SRC_FOLDER = path.join('.fdk', 'temp-theme');
static VUE_CLI_CONFIG_PATH = path.join('.fdk', 'vue.config.js');
static REACT_CLI_CONFIG_PATH = 'webpack.config.js';
@@ -119,7 +120,6 @@ export default class Theme {
const buildPath = path.join(process.cwd(), Theme.BUILD_FOLDER);
const outputFilePath = path.resolve(buildPath, outputFileName);
-
const bundle = Theme.evaluateBundle(outputFilePath);
const parsed = await bundle({
@@ -271,6 +271,7 @@ export default class Theme {
await inquirer.prompt(accountTypeQuestions).then(async (answers) => {
try {
company_type = answers.accountType;
+ config['accountType'] = answers.accountType;
if (answers.accountType === 'development') {
companyList = await ExtensionService.getDevelopmentAccounts(
1,
@@ -609,7 +610,7 @@ export default class Theme {
spaces: 2,
});
const currentContext = getActiveContext();
- await Theme.syncReactTheme(currentContext);
+ await Theme.syncReactTheme(currentContext, targetDirectory);
var b5 = successBox({
text:
chalk.green.bold('DONE ') +
@@ -787,10 +788,7 @@ export default class Theme {
spaces: 2,
},
);
- if (
- !fs.existsSync(path.join(process.cwd(), THEME_ENTRY_FILE)) &&
- themeData.theme_type === THEME_TYPE.vue2
- ) {
+ if (!fs.existsSync(path.join(process.cwd(), DEV_VUE_THEME_ENTRY_FILE)) && (themeData.theme_type === THEME_TYPE.vue2)) {
Logger.info('Restructuring folder structure');
let restructureSpinner = new Spinner(
'Restructuring folder structure',
@@ -830,6 +828,7 @@ export default class Theme {
};
private static syncReactTheme = async (
currentContext: ThemeContextInterface,
+ targetDirectory?: string
) => {
try {
await Theme.ensureThemeTypeInPackageJson();
@@ -872,6 +871,7 @@ export default class Theme {
assetBasePath,
imageCdnUrl,
isHMREnabled: false,
+ targetDirectory
});
const parsed = await Theme.getThemeBundle(stats);
@@ -1158,7 +1158,7 @@ export default class Theme {
Logger.info(`Locally building`);
Theme.createVueConfig();
await devBuild({
- buildFolder: Theme.BUILD_FOLDER,
+ buildFolder: Theme.SERVE_BUILD_FOLDER,
imageCdnUrl: urlJoin(getFullLocalUrl(port), 'assets/images'),
isProd: isSSR,
});
@@ -1179,7 +1179,7 @@ export default class Theme {
watcher.on('change', async () => {
Logger.info(chalk.bold.green(`building`));
await devBuild({
- buildFolder: Theme.BUILD_FOLDER,
+ buildFolder: Theme.SERVE_BUILD_FOLDER,
imageCdnUrl: urlJoin(
getFullLocalUrl(port),
'assets/images',
@@ -1209,7 +1209,7 @@ export default class Theme {
await Theme.createReactSectionsIndexFile();
await devReactBuild({
- buildFolder: Theme.BUILD_FOLDER,
+ buildFolder: Theme.SERVE_BUILD_FOLDER,
runOnLocal: true,
localThemePort: port,
isHMREnabled,
@@ -1228,7 +1228,7 @@ export default class Theme {
Logger.info(chalk.bold.green(`Watching files for changes`));
devReactWatch(
{
- buildFolder: Theme.BUILD_FOLDER,
+ buildFolder: Theme.SERVE_BUILD_FOLDER,
runOnLocal: true,
localThemePort: port,
isHMREnabled,
@@ -1604,6 +1604,7 @@ export default class Theme {
};
private static clearPreviousBuild = () => {
rimraf.sync(Theme.BUILD_FOLDER);
+ rimraf.sync(Theme.SERVE_BUILD_FOLDER);
rimraf.sync(Theme.SRC_ARCHIVE_FOLDER);
};
@@ -1667,20 +1668,22 @@ export default class Theme {
};
private static getImageCdnBaseUrl = async () => {
- let imageCdnUrl = '';
try {
- let startData = {
- file_name: 'test.jpg',
- content_type: 'image/jpeg',
- size: '1',
- };
- let startAssetData = (
- await UploadService.startUpload(
- startData,
- 'application-theme-images',
- )
- ).data;
- return (imageCdnUrl = path.dirname(startAssetData.cdn.url));
+ const dummyFile = path.join(
+ __dirname,
+ '..',
+ '..',
+ 'sample-upload.jpeg'
+ );
+
+ const response = await UploadService.uploadFile(
+ dummyFile,
+ 'application-theme-images',
+ null,
+ 'image/jpeg'
+ );
+
+ return path.dirname(response.complete.cdn.url);
} catch (err) {
Logger.error(err);
throw new CommandError(
@@ -1691,20 +1694,23 @@ export default class Theme {
};
private static getAssetCdnBaseUrl = async () => {
- let assetCdnUrl = '';
try {
- const startData = {
- file_name: 'test.ttf',
- content_type: 'font/ttf',
- size: '10',
- };
- const startAssetData = (
- await UploadService.startUpload(
- startData,
- 'application-theme-assets',
- )
- ).data;
- return (assetCdnUrl = path.dirname(startAssetData.cdn.url));
+ const dummyFile = path.join(
+ __dirname,
+ '..',
+ '..',
+ 'sample-upload.js'
+ );
+
+ const response = await UploadService.uploadFile(
+ dummyFile,
+ 'application-theme-assets',
+ null,
+ 'application/javascript'
+ );
+
+ return path.dirname(response.complete.cdn.url);
+
} catch (err) {
throw new CommandError(
`Failed in getting assets CDN base url`,
@@ -1871,7 +1877,7 @@ export default class Theme {
path.join(process.cwd(), Theme.BUILD_FOLDER, commonJS),
'application-theme-assets',
);
- const commonJsUrl = commonJsUrlRes.start.cdn.url;
+ const commonJsUrl = commonJsUrlRes.complete.cdn.url;
Logger.info('Uploading umdJS');
const umdMinAssets = glob.sync(
@@ -1908,9 +1914,9 @@ export default class Theme {
});
const cssUrls = await Promise.all(cssPromisesArr);
return [
- cssUrls.map((res) => res.start.cdn.url),
+ cssUrls.map((res) => res.complete.cdn.url),
commonJsUrl,
- umdJsUrls.map((res) => res.start.cdn.url),
+ umdJsUrls.map((res) => res.complete.cdn.url),
];
} catch (err) {
throw new CommandError(
@@ -2225,28 +2231,32 @@ export default class Theme {
let settingProps;
const customRoutes = (ctTemplates, parentKey = null) => {
for (let key in ctTemplates) {
- const routerPath =
- (parentKey && `${parentKey}/${key}`) || `c/${key}`;
- const value = routerPath.replace(/\//g, ':::');
- if (
- ctTemplates[key].component &&
- ctTemplates[key].component.__settings
- ) {
- settingProps =
- ctTemplates[key].component.__settings.props;
- }
- customFiles[value] = {
- fileSetting: settingProps,
- value,
- text: pageNameModifier(key),
- path: routerPath,
- };
-
- if (
- ctTemplates[key].children &&
- Object.keys(ctTemplates[key].children).length
- ) {
- customRoutes(ctTemplates[key].children, routerPath);
+ if (/^[\/:a-zA-Z0-9_-]+$/.test(key)) {
+ const routerPath =
+ (parentKey && `${parentKey}/${key}`) || `c/${key}`;
+ const value = routerPath.replace(/\//g, ':::');
+ if (
+ ctTemplates[key].component &&
+ ctTemplates[key].component.__settings
+ ) {
+ settingProps =
+ ctTemplates[key].component.__settings.props;
+ }
+ customFiles[value] = {
+ fileSetting: settingProps,
+ value,
+ text: pageNameModifier(key),
+ path: routerPath,
+ };
+
+ if (
+ ctTemplates[key].children &&
+ Object.keys(ctTemplates[key].children).length
+ ) {
+ customRoutes(ctTemplates[key].children, routerPath);
+ }
+ }else{
+ throw new Error(`Found an invalid custom page URL: ${key}`);
}
}
};
@@ -2515,7 +2525,7 @@ export default class Theme {
zipFilePath,
'application-theme-src',
);
- return res.start.cdn.url;
+ return res.complete.cdn.url;
} catch (err) {
throw new CommandError(
err.message || `Failed to upload src folder`,
diff --git a/src/lib/Tunnel.ts b/src/lib/Tunnel.ts
index 4a44e21f..6870d45c 100644
--- a/src/lib/Tunnel.ts
+++ b/src/lib/Tunnel.ts
@@ -15,10 +15,10 @@ export default class Tunnel {
tunnelProcess: ChildProcess;
stopTunnel: (signal?: NodeJS.Signals | number) => boolean;
- constructor(options){
+ constructor(options: typeof this.options){
this.options = options;
}
-
+
public static async tunnelHandler(options){
const tunnel = new Tunnel(options);
@@ -42,6 +42,7 @@ export default class Tunnel {
spinner.succeed();
return this.publicTunnelURL;
} catch (error) {
+ Debug(error);
spinner.fail();
throw new CommandError(
ErrorCodes.ClOUDFLARE_CONNECTION_ISSUE.message,
diff --git a/src/lib/api/services/extension.service.ts b/src/lib/api/services/extension.service.ts
index f6178d15..f429239f 100644
--- a/src/lib/api/services/extension.service.ts
+++ b/src/lib/api/services/extension.service.ts
@@ -155,7 +155,7 @@ export default {
throw error;
}
},
-
+
getExtensionList: async (page_no: number, page_size: number) => {
try {
let axiosOptions = Object.assign({}, getCommonHeaderOptions());
@@ -168,4 +168,155 @@ export default {
throw error;
}
},
+
+ // Extension Section
+
+ publishExtensionBindings: async (
+ extensionId: string,
+ organisationId: string,
+ data : any
+ ) => {
+ try {
+ const axiosOption = Object.assign(
+ {},
+ { data },
+ {
+ headers: {
+ ...getCommonHeaderOptions().headers,
+ 'x-application-data': '{}',
+ 'x-user-data': '{}',
+ }
+ },
+ );
+
+ const res = await ApiClient.post(
+ URLS.PUBLISH_SECTIONS(extensionId, organisationId),
+ axiosOption,
+ );
+
+ return res.data;
+ } catch (error) {
+ throw error;
+ }
+ },
+
+ previewExtensionBindings: async (
+ extensionId: string,
+ organisationId: string,
+ data : any
+ ) => {
+ try {
+
+ const axiosOption = Object.assign(
+ {},
+ { data },
+ {
+ headers: {
+ ...getCommonHeaderOptions().headers,
+ 'x-application-data': '{}',
+ 'x-user-data': '{}',
+ }
+ },
+ );
+
+ const res = await ApiClient.post(
+ URLS.PREVIEW_SECTIONS(extensionId, organisationId),
+ axiosOption,
+ );
+
+ return res.data;
+ } catch (error) {
+ throw error;
+ }
+ },
+
+ deleteExtensionBindings: async (
+ extensionId: string,
+ organisationId: string,
+ data : any
+ ) => {
+ try {
+
+ const axiosOption = Object.assign(
+ {},
+ { data },
+ {
+ headers: {
+ ...getCommonHeaderOptions().headers,
+ 'x-application-data': '{}',
+ 'x-user-data': '{}',
+ }
+ },
+ );
+
+ const res = await ApiClient.del(
+ URLS.DELETE_SECTIONS(extensionId, organisationId),
+ axiosOption,
+ );
+
+ return res.data;
+ } catch (error) {
+ throw error;
+ }
+ },
+
+ draftExtensionBindings: async (
+ extensionId: string,
+ organisationId: string,
+ data : any
+ ) => {
+ try {
+ const axiosOption = Object.assign(
+ {},
+ { data },
+ {
+ headers: {
+ ...getCommonHeaderOptions().headers,
+ 'x-application-data': '{}',
+ 'x-user-data': '{}',
+ }
+ },
+ );
+
+ const res = await ApiClient.post(
+ URLS.DRAFT_SECTIONS(extensionId, organisationId),
+ axiosOption,
+ );
+
+ return res.data;
+ } catch (error) {
+ throw error;
+ }
+ },
+
+ getExtensionBindings: async (
+ extension_id: string,
+ organization_id: string,
+ binding_name : string,
+ accountType : string,
+ framework: string,
+ ) => {
+ try {
+ const axiosOption = Object.assign(
+ {},
+ { },
+ {
+ headers: {
+ ...getCommonHeaderOptions().headers,
+ 'x-application-data': '{}',
+ 'x-user-data': '{}',
+ }
+ },
+ );
+
+ const res = await ApiClient.get(
+ URLS.GET_EXTENSION_SECTIONS(extension_id, organization_id, binding_name, accountType, framework),
+ axiosOption,
+ );
+
+ return res.data;
+ } catch (error) {
+ throw error;
+ }
+ },
};
diff --git a/src/lib/api/services/theme.service.ts b/src/lib/api/services/theme.service.ts
index 560a2551..0cb79950 100644
--- a/src/lib/api/services/theme.service.ts
+++ b/src/lib/api/services/theme.service.ts
@@ -239,4 +239,16 @@ export default {
throw err;
}
},
+ getAppliedTheme: async (data) => {
+ try {
+ const axiosOption = Object.assign({}, getCommonHeaderOptions());
+ const res = await ApiClient.get(
+ URLS.GET_APPLIED_THEME(data.company_id, data.application_id),
+ axiosOption,
+ );
+ return res?.data;
+ } catch (err) {
+ throw err;
+ }
+ },
};
diff --git a/src/lib/api/services/upload.service.ts b/src/lib/api/services/upload.service.ts
index 9f0ba2d3..929f8b71 100644
--- a/src/lib/api/services/upload.service.ts
+++ b/src/lib/api/services/upload.service.ts
@@ -8,25 +8,8 @@ import mime from 'mime';
import Spinner from '../../../helper/spinner';
export default {
- startUpload: async (data, namespace) => {
- try {
- const axiosOption = Object.assign(
- {},
- {
- data: data,
- },
- getCommonHeaderOptions(),
- );
- const res = await ApiClient.post(
- URLS.START_UPLOAD_FILE(namespace),
- axiosOption,
- );
- return res;
- } catch (error) {
- throw error;
- }
- },
- uploadFile: async (filepath, namespace, file_name = null) => {
+
+ uploadFile: async (filepath, namespace, file_name = null, mimeType = null) => {
let spinner = new Spinner();
let textMessage;
try {
@@ -35,7 +18,8 @@ export default {
filepath,
)} [${Math.round(stats.size / 1024)} KB]`;
spinner.start(textMessage);
- let contentType = mime.getType(path.extname(filepath));
+ let contentType = mimeType || mime.getType(path.extname(filepath));
+
if (contentType === 'image/jpg') {
contentType = 'image/jpeg';
}
diff --git a/src/lib/api/services/url.ts b/src/lib/api/services/url.ts
index 0712a8a2..f06b3fc1 100644
--- a/src/lib/api/services/url.ts
+++ b/src/lib/api/services/url.ts
@@ -99,6 +99,13 @@ export const URLS = {
);
},
+ GET_APPLIED_THEME: (company_id: number, application_id: string) => {
+ return urlJoin(
+ THEME_URL(),
+ `organization/${getOrganizationId()}/company/${company_id}/application/${application_id}/applied-theme`,
+ );
+ },
+
// AVAILABLE_PAGE
AVAILABLE_PAGE: (
application_id: string,
@@ -173,6 +180,37 @@ export const URLS = {
);
},
+ // Extension Section
+ PUBLISH_SECTIONS: (extension_id: string, organization_id) => {
+ return urlJoin(
+ THEME_URL(),
+ `organization/${organization_id}/extension-section/${extension_id}/publish`,
+ );
+ },
+ DRAFT_SECTIONS: (extension_id: string, organization_id) => {
+ return urlJoin(
+ THEME_URL(),
+ `organization/${organization_id}/extension-section/${extension_id}/draft`,
+ );
+ },
+ PREVIEW_SECTIONS: (extension_id: string, organization_id) => {
+ return urlJoin(
+ THEME_URL(),
+ `organization/${organization_id}/extension-section/${extension_id}/preview`,
+ );
+ },
+ DELETE_SECTIONS: (extension_id: string, organization_id) => {
+ return urlJoin(
+ THEME_URL(),
+ `organization/${organization_id}/extension-section/${extension_id}/preview`,
+ );
+ },
+ GET_EXTENSION_SECTIONS: (extension_id: string, organization_id: string, binding_name: string, accountType: string, framework: string) => {
+ return urlJoin(
+ THEME_URL(),
+ `organization/${organization_id}/extension-section/${extension_id}/${binding_name}?accountType=${accountType}&type=${framework}`,
+ );
+ },
// Organization
GET_ORGANIZATION_DETAILS: () : string => {
return urlJoin(