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 @@ + + + + + + { + "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) => ( + {media.alt} + ))} +

{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(