├── src ├── templates │ ├── web │ │ ├── src │ │ │ ├── config.json │ │ │ ├── components │ │ │ │ ├── Constants.js │ │ │ │ └── Spinner.js │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ └── utils.js │ │ └── index.html │ ├── extension-registration │ │ ├── rte-widgets.ejs │ │ ├── rte-badges.ejs │ │ ├── rte-toolbar-buttons.ejs │ │ └── header-menu-buttons.ejs │ ├── chatgpt-demo │ │ ├── openai-declaration.ejs │ │ ├── README.md.ejs │ │ └── rte-toolbar-buttons.ejs │ ├── modal.ejs │ ├── hooks │ │ └── post-deploy.js │ ├── widget.ejs │ ├── extension-registration.ejs │ └── app.ejs ├── manifests │ └── demo-extension-manifest.json ├── generators │ ├── demo-extension.js │ ├── config.js │ ├── dependencies.js │ └── web-assets-react.js ├── prompts │ ├── header-menu.js │ └── rte.js ├── utils.js ├── index.js └── prompts.js ├── .eslintignore ├── .babelrc ├── COPYRIGHT ├── .editorconfig ├── install.yml ├── .gitignore ├── jest.config.js ├── .github └── workflows │ ├── on-release-publish-to-npm.yml │ └── run-tests-on-pr.yml ├── .eslintrc.json ├── CONTRIBUTING.md ├── README.md ├── test ├── test-manifests.js ├── jest.setup.js ├── index.test.js └── utils.test.js ├── package.json ├── CODE_OF_CONDUCT.md └── LICENSE /src/templates/web/src/config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/templates/ 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-transform-react-jsx" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/templates/web/src/components/Constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extensionId: '<%- extensionManifest.id %>' 3 | } 4 | -------------------------------------------------------------------------------- /src/templates/web/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | 6 | html, 7 | body { 8 | margin: 0; 9 | } 10 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2022 Adobe 2 | 3 | Adobe holds the copyright for all the files found in this repository. 4 | 5 | See the LICENSE file for licensing information. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /install.yml: -------------------------------------------------------------------------------- 1 | $schema: http://json-schema.org/draft-07/schema 2 | $id: https://adobe.io/schemas/app-builder-templates/1 3 | 4 | categories: 5 | - action 6 | - ui 7 | 8 | extensions: 9 | - extensionPointId: aem/cf-editor/1 10 | -------------------------------------------------------------------------------- /src/templates/web/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | import 'core-js/stable' 6 | 7 | window.React = require('react') 8 | import ReactDOM from 'react-dom' 9 | 10 | import App from './components/App' 11 | import './index.css' 12 | 13 | ReactDOM.render( 14 | , 15 | document.getElementById('root') 16 | ) 17 | -------------------------------------------------------------------------------- /src/manifests/demo-extension-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatGPT rich text editor sample extension", 3 | "id": "chatgpt-editor-extension-demo", 4 | "description": "The extensions adds ChatGPT support to rich text editor in CF Editor", 5 | "version": "1.0.0", 6 | "isDemoExtension": true, 7 | "useReact": true, 8 | "templateDotEnvVars": ["OPENAI_API_KEY", "OPENAI_ORG"] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Package Directories 3 | node_modules 4 | 5 | # Dependency Management 6 | package-lock.json 7 | 8 | # App Builder 9 | .env* 10 | .aio 11 | 12 | # Test Output 13 | junit.xml 14 | 15 | # IDE & Temp 16 | .cache 17 | .idea 18 | .nyc_output 19 | .vscode 20 | coverage 21 | 22 | # Parcel 23 | .parcel-cache 24 | 25 | # OSX 26 | .DS_Store 27 | 28 | # Yeoman 29 | .yo-repository 30 | -------------------------------------------------------------------------------- /src/templates/extension-registration/rte-widgets.ejs: -------------------------------------------------------------------------------- 1 | getWidgets: () => ([ 2 | // @todo YOUR RTE WIDGETS DECLARATION SHOULD BE HERE 3 | <%_ extensionManifest.rte.widgets.forEach((widget) => { -%> 4 | { 5 | id: "<%- widget.id %>", 6 | label: "<%- widget.label %>", 7 | url: "/index.html#/<%- widget.id %>-widget", 8 | }, 9 | <%_ }) -%> 10 | ]), 11 | -------------------------------------------------------------------------------- /src/templates/web/src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | import React from "react"; 6 | import { 7 | Flex, 8 | ProgressCircle, 9 | } from "@adobe/react-spectrum"; 10 | 11 | export default function Spinner(props) { 12 | return ( 13 | 14 | 15 | {props.children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/templates/extension-registration/rte-badges.ejs: -------------------------------------------------------------------------------- 1 | getBadges: () => ([ 2 | // @todo YOUR RTE BADGES DECLARATION SHOULD BE HERE 3 | <%_ extensionManifest.rte.badges.forEach((badge) => { -%> 4 | { 5 | id: "<%- badge.id %>", 6 | prefix: "!", 7 | suffix: "!", 8 | backgroundColor: "#D6F1FF", 9 | textColor: "#54719B", 10 | }, 11 | <%_ }) -%> 12 | ]), 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | verbose: true, 4 | setupFilesAfterEnv: ['./test/jest.setup.js'], 5 | collectCoverage: true, 6 | collectCoverageFrom: [], 7 | testPathIgnorePatterns: [ 8 | '/node_modules/', 9 | ], 10 | coverageThreshold: { 11 | global: { 12 | branches: 25, 13 | lines: 50, 14 | statements: 50, 15 | }, 16 | }, 17 | coveragePathIgnorePatterns: [ 18 | '/src/prompts.js', 19 | '/src/prompts', 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/on-release-publish-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: on-release-publish-to-npm 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | if: github.repository_owner == 'adobe' 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 18 14 | - run: npm install 15 | - uses: JS-DevTools/npm-publish@v1 16 | with: 17 | token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 18 | access: 'public' 19 | -------------------------------------------------------------------------------- /src/templates/extension-registration/rte-toolbar-buttons.ejs: -------------------------------------------------------------------------------- 1 | // @todo YOUR RTE TOOLBAR BUTTONS DECLARATION SHOULD BE HERE 2 | <%_ extensionManifest.rte.toolbarButtons.forEach((button) => { -%> 3 | { 4 | id: "<%- button.id %>", 5 | tooltip: "<%- button.tooltip %>", 6 | icon: 'Airplane', 7 | onClick: (state) => { 8 | return [{ 9 | type: "replaceContent", 10 | value: state.html + "

some new HTML

" 11 | }]; 12 | }, 13 | }, 14 | <%_ }) -%> 15 | -------------------------------------------------------------------------------- /src/templates/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%- extensionManifest.name %> 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": [ 4 | "@adobe/eslint-config-aio-lib-config" 5 | ], 6 | "rules": { 7 | "semi": ["error", "always"], 8 | "comma-dangle": [ 9 | "error", 10 | { 11 | "arrays": "always-multiline", 12 | "objects": "always-multiline", 13 | "imports": "always-multiline", 14 | "exports": "always-multiline", 15 | "functions": "never" 16 | } 17 | ], 18 | "jest/expect-expect": "off", 19 | "jsdoc/require-param-type": "off", 20 | "jsdoc/require-param-description": "off", 21 | "jsdoc/require-returns": "off", 22 | "node/no-unpublished-require": [ 23 | "error", 24 | { 25 | "allowModules": [ 26 | "yeoman-test", 27 | "inquirer", 28 | "stdout-stderr" 29 | ] 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/templates/chatgpt-demo/openai-declaration.ejs: -------------------------------------------------------------------------------- 1 | <%_ if (extensionManifest.isDemoExtension) { -%> 2 | import { Configuration, OpenAIApi } from "openai"; 3 | 4 | const configuration = new Configuration({ 5 | organization: process.env.OPENAI_ORG, 6 | apiKey: process.env.OPENAI_API_KEY, 7 | }); 8 | const openai = new OpenAIApi(configuration); 9 | 10 | const checkState = (state) => { 11 | if (!state.selectedText) { 12 | throw new Error("Please select some text first"); 13 | } 14 | }; 15 | 16 | const makeRequest = async (prompt) => { 17 | try { 18 | const result = await openai.createCompletion({ 19 | model: "text-davinci-003", 20 | prompt: prompt, 21 | max_tokens: 2000 22 | }, { timeout: 20000 }); 23 | return result.data.choices[0].text; 24 | } catch (e) { 25 | throw new Error("An unexpected error has occurred. Please try again"); 26 | } 27 | }; 28 | <%_ } -%> -------------------------------------------------------------------------------- /src/templates/extension-registration/header-menu-buttons.ejs: -------------------------------------------------------------------------------- 1 | headerMenu: { 2 | getButtons() { 3 | return [ 4 | // @todo YOUR HEADER BUTTONS DECLARATION SHOULD BE HERE 5 | <%_ extensionManifest.headerMenuButtons.forEach((button) => { -%> 6 | { 7 | id: '<%- button.id %>', 8 | label: '<%- button.label %>', 9 | icon: 'OpenIn', 10 | onClick() { 11 | <%_ if (button.needsModal) { -%> 12 | const modalURL = "/index.html#/<%- button.id %>-modal"; 13 | console.log("Modal URL: ", modalURL); 14 | 15 | guestConnection.host.modal.showUrl({ 16 | title: "<%- button.label %>", 17 | url: modalURL, 18 | }); 19 | <%_ } else { -%> 20 | console.log("Button '<%- button.id %>' has been pressed."); 21 | <%_ } -%> 22 | }, 23 | }, 24 | <%_ }) -%> 25 | ]; 26 | }, 27 | }, 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 10 | 11 | ## Contributor License Agreement 12 | 13 | All third-party contributions to this project must be accompanied by a signed contributor license agreement. This gives Adobe permission to redistribute your contributions as part of the project. [Sign our CLA](http://opensource.adobe.com/cla.html). You only need to submit an Adobe CLA one time, so if you have submitted one previously, you are good to go! 14 | 15 | ## Code Reviews 16 | 17 | All submissions should come in the form of pull requests and need to be reviewed by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) for more information on sending pull requests. 18 | 19 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when submitting a pull request! 20 | -------------------------------------------------------------------------------- /src/templates/modal.ejs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | import React, { useState, useEffect } from "react"; 6 | import { attach } from "@adobe/uix-guest"; 7 | import { 8 | Flex, 9 | Provider, 10 | Content, 11 | defaultTheme, 12 | Text, 13 | ButtonGroup, 14 | Button, 15 | } from "@adobe/react-spectrum"; 16 | 17 | import { extensionId } from "./Constants"; 18 | 19 | export default function () { 20 | const [guestConnection, setGuestConnection] = useState(); 21 | 22 | useEffect(() => { 23 | (async () => { 24 | const guestConnection = await attach({ id: extensionId }); 25 | 26 | setGuestConnection(guestConnection); 27 | })(); 28 | }, []); 29 | 30 | const onCloseHandler = () => { 31 | guestConnection.host.modal.close(); 32 | }; 33 | 34 | return ( 35 | 36 | 37 | Your modal content for "<%- button.label %>" 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-on-pr.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: run-tests-on-pr 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | on: 11 | push: 12 | branches: [ main ] 13 | pull_request: 14 | branches: [ main ] 15 | 16 | jobs: 17 | build: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | node-version: [18.x, 20.x] 22 | os: [ubuntu-latest, windows-latest] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | - run: npm i --package-lock --package-lock-only 33 | - run: npm ci 34 | - run: npm run build --if-present 35 | - run: npm test 36 | - name: upload coverage 37 | if: success() 38 | run: curl -s https://codecov.io/bash | bash 39 | env: 40 | CODECOV_NAME: ${{ runner.os }} node.js ${{ matrix.node-version }} 41 | shell: bash 42 | -------------------------------------------------------------------------------- /src/templates/chatgpt-demo/README.md.ejs: -------------------------------------------------------------------------------- 1 | # ChatGPT rich text editor sample extension - <%- extensionManifest.name %> 2 | 3 | This extensions adds ChatGPT support to rich text editor in CF Editor. 4 | 5 | <%- extensionManifest.description %> 6 | 7 | ## Setup 8 | 9 | - Populate the `.env` file in the project root and fill it as shown [below](#env) 10 | 11 | ## Local Dev 12 | 13 | - `aio app run` to start your local Dev server 14 | - App will run on `localhost:9080` by default 15 | 16 | By default, the UI will be served locally but actions will be deployed and served from Adobe I/O Runtime. To start a 17 | local serverless stack and also run your actions locally use the `aio app run --local` option. 18 | 19 | ## Test & Coverage 20 | 21 | - Run `aio app test` to run unit tests for ui and actions 22 | - Run `aio app test --e2e` to run e2e tests 23 | 24 | ## Deploy & Cleanup 25 | 26 | - `aio app deploy` to build and deploy all actions on Runtime and static files to CDN 27 | - `aio app undeploy` to undeploy the app 28 | 29 | ## Config 30 | 31 | ### `.env` 32 | 33 | Add or fill `OPENAI` credentials in `.env` file 34 | 35 | ```bash 36 | # This file must **not** be committed to source control 37 | 38 | ## please provide your Adobe I/O Runtime credentials 39 | # AIO_RUNTIME_AUTH= 40 | # AIO_RUNTIME_NAMESPACE= 41 | # OPENAI_API_KEY= 42 | # OPENAI_ORG= 43 | ``` 44 | -------------------------------------------------------------------------------- /src/templates/hooks/post-deploy.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const fs = require('fs'); 3 | const yaml = require('js-yaml'); 4 | 5 | module.exports = (config) => { 6 | try { 7 | // read the app.config.yaml file to get the extension points 8 | const yamlFile = fs.readFileSync(`${config.root}/app.config.yaml`, 'utf8'); 9 | const yamlData = yaml.load(yamlFile); 10 | const { extensions } = yamlData; 11 | 12 | // For now we are ok just to read the first extension point to build the preview link 13 | const extension = Object.keys(extensions)[0]; 14 | const previewData = { 15 | extensionPoint: extension, 16 | url: config.project.workspace.app_url, 17 | }; 18 | 19 | // buid the preview URL 20 | const base64EncodedData = Buffer.from(JSON.stringify(previewData)).toString('base64'); 21 | console.log(chalk.magenta(chalk.bold('For a developer preview of your UI extension in the AEM environment, follow the URL:'))); 22 | 23 | // check if the environment is stage, if so, we need to add the -stage suffix to the URL 24 | const env = process.env.AIO_CLI_ENV === 'stage' ? '-stage' : ''; 25 | console.log(chalk.magenta(chalk.bold(` -> https://experience${env}.adobe.com/aem/extension-manager/preview/${base64EncodedData}`))); 26 | } catch (error) { 27 | // if something went wrong, we do nothing, and just don't display the URL 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/templates/web/src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | /* global fetch */ 6 | 7 | /** 8 | * 9 | * Invokes a web action 10 | * 11 | * @param {string} actionUrl 12 | * @param {object} headers 13 | * @param {object} params 14 | * 15 | * @returns {Promise} the response 16 | * 17 | */ 18 | 19 | async function actionWebInvoke (actionUrl, headers = {}, params = {}, options = { method: 'POST' }) { 20 | const actionHeaders = { 21 | 'Content-Type': 'application/json', 22 | ...headers 23 | } 24 | 25 | const fetchConfig = { 26 | headers: actionHeaders 27 | } 28 | 29 | if (window.location.hostname === 'localhost') { 30 | actionHeaders['x-ow-extra-logging'] = 'on' 31 | } 32 | 33 | fetchConfig.method = options.method.toUpperCase() 34 | 35 | if (fetchConfig.method === 'GET') { 36 | actionUrl = new URL(actionUrl) 37 | Object.keys(params).forEach(key => actionUrl.searchParams.append(key, params[key])) 38 | } else if (fetchConfig.method === 'POST') { 39 | fetchConfig.body = JSON.stringify(params) 40 | } 41 | 42 | const response = await fetch(actionUrl, fetchConfig) 43 | 44 | let content = await response.text() 45 | 46 | if (!response.ok) { 47 | return JSON.parse(content) 48 | } 49 | try { 50 | content = JSON.parse(content) 51 | } catch (e) { 52 | // response is not json 53 | } 54 | return content 55 | } 56 | 57 | export default actionWebInvoke 58 | -------------------------------------------------------------------------------- /src/templates/widget.ejs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | import React, { useEffect, useState } from "react"; 6 | import { attach } from "@adobe/uix-guest"; 7 | import { ComboBox, Item, Provider, lightTheme } from "@adobe/react-spectrum"; 8 | import { extensionId } from "./Constants"; 9 | 10 | export default function () { 11 | const [guestConnection, setGuestConnection] = useState(); 12 | 13 | useEffect(() => { 14 | (async () => { 15 | const guestConnection = await attach({ id: extensionId }); 16 | 17 | setGuestConnection(guestConnection); 18 | })(); 19 | }, []); 20 | 21 | const onSelectionChangeHandler = async (variable) => { 22 | const selectedOption = options.find(el => el.id == variable); 23 | 24 | await guestConnection.host.rte.applyInstructions([{ 25 | type: "insertContent", 26 | value: '{' + selectedOption.name + '}' 27 | }]); 28 | guestConnection.host.rte.closeWidget(); 29 | }; 30 | 31 | const options = [ 32 | {id: "1", name: 'Aerospace'}, 33 | {id: "2", name: 'Mechanical'}, 34 | {id: "3", name: 'Civil'}, 35 | ]; 36 | 37 | return ( 38 | 39 | 43 | {item => {item.name}} 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | # @adobe/aem-cf-editor-ui-ext-tpl 14 | 15 | Extensibility Template for AEM Content Fragment Editor 16 | 17 | # Prerequisites 18 | - `nodejs` (v18) and `npm` installed locally - https://nodejs.org/en/ 19 | - `aio` command line tool - https://github.com/adobe/aio-cli, https://developer.adobe.com/runtime/docs/guides/tools/cli_install/ 20 | - Project in Adobe developer console 21 | 22 | # Installation 23 | - `npm install -g @adobe/aio-cli` 24 | 25 | # Usage 26 | - `aio app init ` 27 | 28 | # Disclaimer 29 | The use of this template via `aio app init` command requires certain npm packages that are only available in private Adobe repositories. In the future, those npm packages will be made available in the npm public registry. 30 | 31 | # Contributing 32 | Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information. 33 | 34 | 35 | # Licensing 36 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. 37 | -------------------------------------------------------------------------------- /test/test-manifests.js: -------------------------------------------------------------------------------- 1 | const defaultExtensionManifest = { 2 | name: 'CF Editor Test Extension', 3 | id: 'cf-editor-test-extension', 4 | description: 'Test Extension for AEM Content Fragment Editor', 5 | version: '0.0.1', 6 | }; 7 | 8 | const customExtensionManifest = { 9 | name: 'CF Editor Test Extension', 10 | id: 'cf-editor-test-extension', 11 | description: 'Test Extension for AEM Content Fragment Editor', 12 | version: '1.0.1', 13 | headerMenuButtons: [ 14 | { 15 | label: 'Import', 16 | needsModal: true, 17 | id: 'import', 18 | }, 19 | { 20 | label: 'Export', 21 | needsModal: false, 22 | id: 'export', 23 | }, 24 | ], 25 | rte: { 26 | badges: [ 27 | { 28 | id: 'badge-1', 29 | }, 30 | { 31 | id: 'badge2', 32 | }, 33 | ], 34 | toolbarButtons: [ 35 | { 36 | id: 'tb-button-1', 37 | tooltip: 'tb button 1 tooltip', 38 | }, 39 | ], 40 | widgets: [ 41 | { 42 | label: 'Widget-1', 43 | id: 'widget-1', 44 | }, 45 | { 46 | label: 'Widget-2', 47 | id: 'widget2', 48 | }, 49 | ], 50 | }, 51 | }; 52 | 53 | const demoExtensionManifest = { 54 | name: 'ChatGPT rich text editor sample extension', 55 | id: 'chatgpt-editor-extension-demo', 56 | description: 'This extensions adds ChatGPT support to rich text editor in CF Editor', 57 | version: '1.0.0', 58 | isDemoExtension: true, 59 | templateDotEnvVars: ['OPENAI_API_KEY', 'OPENAI_ORG'], 60 | }; 61 | 62 | module.exports = { 63 | defaultExtensionManifest, 64 | customExtensionManifest, 65 | demoExtensionManifest, 66 | }; 67 | -------------------------------------------------------------------------------- /src/generators/demo-extension.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const Generator = require('yeoman-generator'); 13 | const { utils } = require('@adobe/generator-app-common-lib'); 14 | const path = require('path'); 15 | 16 | /** 17 | * Generate some additional things related only to demo extension 18 | */ 19 | class DemoExtensionGenerator extends Generator { 20 | constructor (args, opts) { 21 | super(args, opts); 22 | 23 | this.option('extensionOptions', { type: Object }); 24 | } 25 | 26 | writing () { 27 | this.addDependencies(); 28 | this.copyReadme(); 29 | } 30 | 31 | addDependencies () { 32 | utils.addDependencies(this, { 33 | openai: '^3.1.0', 34 | }); 35 | } 36 | 37 | copyReadme () { 38 | this.fs.copyTpl( 39 | this.templatePath( 40 | path.resolve(__dirname, '..', 'templates', this.options.extensionOptions.demoExtensionTemplatesFolder, 'README.md.ejs') 41 | ), 42 | this.destinationPath('README.md'), 43 | { 44 | extensionManifest: this.options.extensionOptions.manifest, 45 | } 46 | ); 47 | } 48 | } 49 | 50 | module.exports = DemoExtensionGenerator; 51 | -------------------------------------------------------------------------------- /test/jest.setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | jest.setTimeout(360000); 13 | 14 | const path = require('path'); 15 | const { stdout, stderr } = require('stdout-stderr'); 16 | 17 | process.on('unhandledRejection', error => { 18 | throw error; 19 | }); 20 | 21 | beforeEach(() => { 22 | stdout.start(); 23 | stderr.start(); 24 | stdout.print = true; // set to true to see output 25 | }); 26 | 27 | afterEach(() => { 28 | stdout.stop(); 29 | stderr.stop(); 30 | }); 31 | 32 | // quick normalization to test windows/unix paths 33 | global.n = p => path.normalize(p); 34 | global.r = p => path.resolve(p); 35 | 36 | /** 37 | * Checks that package.json has all needed dependencies specified. 38 | * 39 | * @param fs 40 | * @param {object} dependencies An object representing expected package.json dependencies. 41 | * @param {object} devDependencies An object representing expected package.json dev dependencies. 42 | */ 43 | global.assertDependencies = (fs, dependencies, devDependencies) => { 44 | expect(JSON.parse(fs.readFileSync('package.json').toString())).toEqual(expect.objectContaining({ 45 | dependencies, 46 | devDependencies, 47 | })); 48 | }; 49 | -------------------------------------------------------------------------------- /src/prompts/header-menu.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License") 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const inquirer = require('inquirer'); 13 | const { generateUniqueWithinListIdFromValue } = require('../utils'); 14 | 15 | const headerMenuButtonPrompts = (manifest) => { 16 | const questions = [labelPrompt(), modalPrompt()]; 17 | 18 | return inquirer 19 | .prompt(questions) 20 | .then((answers) => { 21 | manifest.headerMenuButtons = manifest.headerMenuButtons || []; 22 | answers.id = generateUniqueWithinListIdFromValue(answers.label, manifest.headerMenuButtons); 23 | manifest.headerMenuButtons.push(answers); 24 | }) 25 | .catch((error) => { 26 | console.error(error); 27 | }); 28 | }; 29 | 30 | const labelPrompt = () => { 31 | return { 32 | type: 'input', 33 | name: 'label', 34 | message: 'Please provide label name for the button:', 35 | validate: (value) => value.length ? true : 'Required.', 36 | }; 37 | }; 38 | 39 | const modalPrompt = () => { 40 | return { 41 | type: 'confirm', 42 | name: 'needsModal', 43 | message: 'Do you need to show a modal for the button?', 44 | default: false, 45 | }; 46 | }; 47 | 48 | module.exports = headerMenuButtonPrompts; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-cf-editor-ui-ext-tpl", 3 | "version": "2.1.0", 4 | "main": "src/index.js", 5 | "description": "Extensibility template for AEM Content Fragment Editor", 6 | "engines": { 7 | "node": ">=18.0.0" 8 | }, 9 | "keywords": [ 10 | "ecosystem:aio-app-builder-templates" 11 | ], 12 | "author": "Adobe Inc.", 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "@adobe/eslint-config-aio-lib-config": "^2.0.0", 16 | "@adobe/generator-app-common-lib": "^0.3.3", 17 | "chalk": "^4.0.0", 18 | "fs-extra": "^10.1.0", 19 | "slugify": "^1.6.5", 20 | "upath": "^2.0.1", 21 | "yeoman-environment": "^3", 22 | "yeoman-generator": "^5" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.8.7", 26 | "@babel/eslint-parser": "^7.21.8", 27 | "@babel/plugin-transform-react-jsx": "^7.8.3", 28 | "@babel/polyfill": "^7.8.7", 29 | "@babel/preset-env": "^7.8.7", 30 | "@types/jest": "^28.1.8", 31 | "eol": "^0.9.1", 32 | "eslint": "^8", 33 | "eslint-config-standard": "^17", 34 | "eslint-plugin-import": "^2", 35 | "eslint-plugin-jest": "^27", 36 | "eslint-plugin-node": "^11", 37 | "eslint-plugin-promise": "^6", 38 | "eslint-plugin-standard": "^4.0.1", 39 | "inquirer": "^8.2.5", 40 | "jest": "^28", 41 | "js-yaml": "^4.1.0", 42 | "lodash.clonedeep": "^4.5.0", 43 | "stdout-stderr": "^0.1.13", 44 | "yeoman-assert": "^3.1.1", 45 | "yeoman-environment": "^3.2.0", 46 | "yeoman-test": "^6.3.0" 47 | }, 48 | "peerDependencies": { 49 | "@adobe/aio-cli-plugin-app-templates": "^1.0.0" 50 | }, 51 | "scripts": { 52 | "test": "jest ./test", 53 | "lint:check": "eslint --ext .js .", 54 | "lint:fix": "eslint --ext .js --fix ." 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const helpers = require('yeoman-test'); 13 | const MainGenerator = require('../src/index'); 14 | const { defaultExtensionManifest, customExtensionManifest, demoExtensionManifest } = require('./test-manifests'); 15 | 16 | describe('run', () => { 17 | test('test a generator invocation with default code generation', async () => { 18 | const options = { 19 | 'skip-prompt': true, 20 | 'skip-install': true, 21 | 'extension-manifest': defaultExtensionManifest, 22 | }; 23 | await helpers.run(MainGenerator).withOptions(options); 24 | }); 25 | 26 | test('test a generator invocation with custom code generation', async () => { 27 | const options = { 28 | 'skip-prompt': true, 29 | 'skip-install': true, 30 | 'extension-manifest': customExtensionManifest, 31 | }; 32 | await helpers.run(MainGenerator).withOptions(options); 33 | }); 34 | 35 | test('test a generator invocation with demo code generation', async () => { 36 | const options = { 37 | 'skip-prompt': true, 38 | 'skip-install': true, 39 | 'extension-manifest': demoExtensionManifest, 40 | }; 41 | await helpers.run(MainGenerator).withOptions(options); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/templates/extension-registration.ejs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | import { Text } from "@adobe/react-spectrum"; 6 | import { register } from "@adobe/uix-guest"; 7 | import { extensionId } from "./Constants"; 8 | import metadata from '../../../../app-metadata.json'; 9 | <%_ if (extensionManifest.isDemoExtension) { -%> 10 | <%- include(demoExtensionTemplatesFolder + '/openai-declaration') -%> 11 | <%_ } -%> 12 | 13 | function ExtensionRegistration() { 14 | const init = async () => { 15 | const guestConnection = await register({ 16 | id: extensionId, 17 | metadata, 18 | methods: { 19 | <%_ if (extensionManifest.headerMenuButtons) { -%> 20 | <%- include('./extension-registration/header-menu-buttons') -%> 21 | <%_ } -%> 22 | <%_ if (extensionManifest.rte || extensionManifest.isDemoExtension) { -%> 23 | 24 | rte: { 25 | <%_ if (extensionManifest.rte && extensionManifest.rte.widgets) { -%> 26 | <%- include('./extension-registration/rte-widgets') -%> 27 | <%_ } -%> 28 | <%_ if (extensionManifest.rte && extensionManifest.rte.badges) { -%> 29 | 30 | <%- include('./extension-registration/rte-badges') -%> 31 | <%_ } -%> 32 | <%_ if ((extensionManifest.rte && extensionManifest.rte.toolbarButtons) || extensionManifest.isDemoExtension) { -%> 33 | 34 | getCustomButtons: () => ([ 35 | <%_ if (extensionManifest.rte && extensionManifest.rte.toolbarButtons) { -%> 36 | <%- include('./extension-registration/rte-toolbar-buttons') -%> 37 | <%_ } -%> 38 | <%_ if (extensionManifest.isDemoExtension) { -%> 39 | 40 | <%- include(demoExtensionTemplatesFolder + '/rte-toolbar-buttons') -%> 41 | <%_ } -%> 42 | ]), 43 | <%_ } -%> 44 | } 45 | <%_ } -%> 46 | } 47 | }); 48 | }; 49 | init().catch(console.error); 50 | 51 | return IFrame for integration with Host (AEM)...; 52 | } 53 | 54 | export default ExtensionRegistration; 55 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License") 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const fs = require('fs-extra'); 13 | const slugify = require('slugify'); 14 | 15 | /** 16 | * @param manifestPath 17 | */ 18 | function readManifest (manifestPath) { 19 | try { 20 | return fs.readJsonSync(manifestPath, { encoding: 'utf8' }); 21 | } catch (err) { 22 | if (err.code && err.code === 'ENOENT') { 23 | return {}; 24 | } else { 25 | throw err; 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * @param manifest 32 | * @param manifestPath 33 | */ 34 | function writeManifest (manifest, manifestPath) { 35 | fs.writeJsonSync(manifestPath, manifest, { spaces: 2 }); 36 | } 37 | 38 | function generateUniqueWithinListIdFromValue (value, list) { 39 | let id = slugify(value, { 40 | lower: true, 41 | strict: true, 42 | }); 43 | 44 | if (!/^[a-zA-Z]/.test(id)) { 45 | id = generateRandomPrefix(5) + '-' + id; 46 | } 47 | 48 | if (list) { 49 | let matching = list.find(obj => obj.id === id); 50 | while (matching !== undefined) { 51 | id = generateRandomPrefix(5) + '-' + id; 52 | matching = list.find(obj => obj.id === id); 53 | } 54 | } 55 | 56 | return id; 57 | } 58 | 59 | function generateRandomPrefix (length) { 60 | const characters = 'abcdefghijklmnopqrstuvwxyz'; 61 | const randomString = []; 62 | 63 | for (let i = 0; i < length; i++) { 64 | const character = characters.charAt(Math.floor(Math.random() * characters.length)); 65 | randomString.push(character); 66 | } 67 | 68 | return randomString.join(''); 69 | } 70 | 71 | module.exports = { 72 | readManifest, 73 | writeManifest, 74 | generateRandomPrefix, 75 | generateUniqueWithinListIdFromValue, 76 | }; 77 | -------------------------------------------------------------------------------- /src/generators/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const Generator = require('yeoman-generator'); 13 | const path = require('path'); 14 | const upath = require('upath'); 15 | const { utils } = require('@adobe/generator-app-common-lib'); 16 | 17 | class ConfigGenerator extends Generator { 18 | constructor (args, opts) { 19 | super(args, opts); 20 | 21 | this.option('extensionOptions', { type: Object }); 22 | } 23 | 24 | writing () { 25 | this.generateDefaultMetadata(); 26 | this.generateAppConfig(); 27 | this.generateExtensionConfig(); 28 | 29 | } 30 | 31 | generateDefaultMetadata () { 32 | this.fs.writeJSON('src/app-metadata.json', {}); 33 | } 34 | 35 | generateAppConfig () { 36 | utils.writeKeyAppConfig( 37 | this, 38 | 'extensions.' + this.options.extensionOptions.type, 39 | { 40 | // posix separator 41 | $include: upath.toUnix(this.options.extensionOptions.configPath), 42 | } 43 | ); 44 | } 45 | 46 | generateExtensionConfig () { 47 | utils.writeKeyYAMLConfig( 48 | this, 49 | this.options.extensionOptions.configPath, 50 | 'operations', 51 | { 52 | view: [ 53 | { type: 'web', impl: 'index.html' }, 54 | ], 55 | } 56 | ); 57 | 58 | utils.writeKeyYAMLConfig( 59 | this, 60 | this.options.extensionOptions.configPath, 61 | 'web', 62 | path.relative(this.options.extensionOptions.rootFolder, this.options.extensionOptions.webSrcFolder) 63 | ); 64 | 65 | // add hooks path 66 | utils.writeKeyYAMLConfig( 67 | this, 68 | this.options.extensionOptions.configPath, 69 | 'hooks', { 70 | 'pre-app-run': 'node node_modules/@adobe/uix-guest/scripts/generate-metadata.js', 71 | 'pre-app-build': 'node node_modules/@adobe/uix-guest/scripts/generate-metadata.js', 72 | 'post-app-deploy': './hooks/post-deploy.js' 73 | } 74 | ); 75 | } 76 | } 77 | 78 | module.exports = ConfigGenerator; 79 | -------------------------------------------------------------------------------- /src/generators/dependencies.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const Generator = require('yeoman-generator'); 13 | const { utils, constants } = require('@adobe/generator-app-common-lib'); 14 | 15 | class DependenciesGenerator extends Generator { 16 | constructor (args, opts) { 17 | super(args, opts); 18 | 19 | this.option('extensionOptions', { type: Object }); 20 | } 21 | 22 | writing () { 23 | this.addBaseDependencies(); 24 | this.addDevDependencies(); 25 | this.addPackageScript(); 26 | } 27 | 28 | /** 29 | * Todo: add dependencies based on "useReact" extension option 30 | */ 31 | addBaseDependencies () { 32 | utils.addDependencies( 33 | this, 34 | { 35 | '@adobe/aio-sdk': constants.commonDependencyVersions['@adobe/aio-sdk'], 36 | '@adobe/exc-app': '^0.2.21', 37 | '@adobe/react-spectrum': '^3.4.0', 38 | '@adobe/uix-guest': '^0.10.0', 39 | '@react-spectrum/list': '^3.0.0-rc.0', 40 | '@spectrum-icons/workflow': '^3.2.0', 41 | 'chalk': '^4', 42 | 'core-js': '^3.6.4', 43 | 'node-fetch': '^2.6.0', 44 | 'node-html-parser': '^5.4.2-0', 45 | react: '^16.13.1', 46 | 'react-dom': '^16.13.1', 47 | 'react-error-boundary': '^1.2.5', 48 | 'react-router-dom': '^6.3.0', 49 | 'regenerator-runtime': '^0.13.5', 50 | } 51 | ); 52 | } 53 | 54 | addDevDependencies () { 55 | utils.addDependencies( 56 | this, 57 | { 58 | '@babel/core': '^7.8.7', 59 | '@babel/plugin-transform-react-jsx': '^7.8.3', 60 | '@babel/polyfill': '^7.8.7', 61 | '@babel/preset-env': '^7.8.7', 62 | '@openwhisk/wskdebug': '^1.3.0', 63 | jest: '^27.2.4', 64 | "ajv": "^8.12.0", 65 | "js-yaml": "^4.1.0" 66 | }, 67 | true 68 | ); 69 | } 70 | 71 | addPackageScript () { 72 | utils.addPkgScript( 73 | this, 74 | { 75 | "transform:yaml-to-json": "node node_modules/@adobe/uix-guest/scripts/generate-metadata.js" 76 | } 77 | ); 78 | } 79 | } 80 | 81 | module.exports = DependenciesGenerator; 82 | -------------------------------------------------------------------------------- /src/templates/app.ejs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | import React from "react"; 6 | import ErrorBoundary from "react-error-boundary"; 7 | import { HashRouter as Router, Routes, Route } from "react-router-dom"; 8 | import ExtensionRegistration from "./ExtensionRegistration"; 9 | <%_ (extensionManifest.headerMenuButtons || []).forEach((button) => { -%> 10 | <%_ if (button.needsModal) { -%> 11 | <%_ const buttonComponent = button.id.replace(/-/g, '') + 'Modal' -%> 12 | <%_ const capitalizedButtonComponent = buttonComponent.charAt(0).toUpperCase() + buttonComponent.slice(1); -%> 13 | import <%- capitalizedButtonComponent %> from "./<%- capitalizedButtonComponent %>"; 14 | <%_ }}) -%> 15 | <%_ if (extensionManifest.rte && extensionManifest.rte.widgets) { -%> 16 | <%_ extensionManifest.rte.widgets.forEach((widget) => { -%> 17 | <%_ const widgetComponent = widget.id.replace(/-/g, '') + 'Widget' -%> 18 | <%_ const capitalizedWidgetComponent = widgetComponent.charAt(0).toUpperCase() + widgetComponent.slice(1); -%> 19 | import <%- capitalizedWidgetComponent %> from "./<%- capitalizedWidgetComponent %>"; 20 | <%_ }) -%> 21 | <%_ } -%> 22 | 23 | function App() { 24 | return ( 25 | 26 | 27 | 28 | } /> 29 | } 32 | /> 33 | <%_ if (extensionManifest.headerMenuButtons) { -%> 34 | <%_ extensionManifest.headerMenuButtons.forEach((button) => { -%> 35 | <%_ if (button.needsModal) { -%> 36 | <%_ const buttonComponent = button.id.replace(/-/g, '') + 'Modal' -%> 37 | <%_ const capitalizedButtonComponent = buttonComponent.charAt(0).toUpperCase() + buttonComponent.slice(1); -%> 38 | />} 41 | /> 42 | <%_ }})} -%> 43 | <%_ if (extensionManifest.rte && extensionManifest.rte.widgets) { -%> 44 | <%_ extensionManifest.rte.widgets.forEach((widget) => { -%> 45 | <%_ const widgetComponent = widget.id.replace(/-/g, '') + 'Widget' -%> 46 | <%_ const capitalizedWidgetComponent = widgetComponent.charAt(0).toUpperCase() + widgetComponent.slice(1); -%> 47 | />} 50 | /> 51 | <%_ }) -%> 52 | <%_ } -%> 53 | {/* @todo YOUR CUSTOM ROUTES SHOULD BE HERE */} 54 | 55 | 56 | 57 | ) 58 | 59 | // Methods 60 | 61 | // error handler on UI rendering failure 62 | function onError(e, componentStack) {} 63 | 64 | // component to show if UI fails rendering 65 | function fallbackComponent({ componentStack, error }) { 66 | return ( 67 | 68 |

69 | Phly, phly... Something went wrong :( 70 |

71 |
{componentStack + "\n" + error.message}
72 |
73 | ); 74 | } 75 | } 76 | 77 | export default App; 78 | -------------------------------------------------------------------------------- /src/prompts/rte.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License") 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const inquirer = require('inquirer'); 13 | const { generateUniqueWithinListIdFromValue } = require('../utils'); 14 | 15 | const toolbarButtonPrompts = (manifest) => { 16 | const idMessage = 'Please provide ID for the toolbar button (must be unique across all extensions, consider' + 17 | ' adding a vendor prefix to this field):'; 18 | const tooltipMessage = 'Please provide tooltip for the button (a text that will be rendered on mouse over):'; 19 | 20 | return inquirer 21 | .prompt([idPrompt(idMessage), tooltipPrompt(tooltipMessage)]) 22 | .then((answers) => { 23 | manifest.rte = manifest.rte || {}; 24 | manifest.rte.toolbarButtons = manifest.rte.toolbarButtons || []; 25 | answers.id = generateUniqueWithinListIdFromValue(answers.id, manifest.rte.toolbarButtons); 26 | manifest.rte.toolbarButtons.push(answers); 27 | }) 28 | .catch((error) => console.error(error)); 29 | }; 30 | 31 | const widgetPrompts = (manifest) => { 32 | const labelMessage = 'Please provide label name for the widget:'; 33 | 34 | return inquirer 35 | .prompt([labelPrompt(labelMessage)]) 36 | .then((answers) => { 37 | manifest.rte = manifest.rte || {}; 38 | manifest.rte.widgets = manifest.rte.widgets || []; 39 | answers.id = generateUniqueWithinListIdFromValue(answers.label, manifest.rte.widgets); 40 | manifest.rte.widgets.push(answers); 41 | }) 42 | .catch((error) => console.error(error)); 43 | }; 44 | 45 | const badgePrompts = (manifest) => { 46 | const idMessage = 'Please provide ID for the badge (must be unique across all extensions, consider adding a vendor' + 47 | ' prefix to this field):'; 48 | 49 | return inquirer 50 | .prompt([idPrompt(idMessage)]) 51 | .then((answers) => { 52 | manifest.rte = manifest.rte || {}; 53 | manifest.rte.badges = manifest.rte.badges || []; 54 | answers.id = generateUniqueWithinListIdFromValue(answers.id, manifest.rte.badges); 55 | manifest.rte.badges.push(answers); 56 | }) 57 | .catch((error) => console.error(error)); 58 | }; 59 | 60 | const idPrompt = (message) => { 61 | return { 62 | type: 'input', 63 | name: 'id', 64 | message, 65 | validate: (answer) => answer.length ? true : 'Required.', 66 | }; 67 | }; 68 | 69 | const tooltipPrompt = (message) => { 70 | return { 71 | type: 'input', 72 | name: 'tooltip', 73 | message, 74 | validate: (answer) => answer.length ? true : 'Required.', 75 | }; 76 | }; 77 | 78 | const labelPrompt = (message) => { 79 | return { 80 | type: 'input', 81 | name: 'label', 82 | message, 83 | validate: (answer) => answer.length ? true : 'Required.', 84 | }; 85 | }; 86 | 87 | module.exports = { 88 | toolbarButtonPrompts, 89 | badgePrompts, 90 | widgetPrompts, 91 | }; 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/generators/web-assets-react.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const path = require('path'); 13 | const Generator = require('yeoman-generator'); 14 | 15 | class WebAssetsReactGenerator extends Generator { 16 | constructor (args, opts) { 17 | super(args, opts); 18 | 19 | this.option('extensionOptions', { type: Object }); 20 | 21 | this.templatesFolder = path.resolve(path.join(__dirname, '../templates/')); 22 | // templateProps are used in ejs templates 23 | this.templateProps = { 24 | extensionManifest: this.options.extensionOptions.manifest, 25 | demoExtensionTemplatesFolder: this.options.extensionOptions.demoExtensionTemplatesFolder, 26 | }; 27 | } 28 | 29 | writing () { 30 | this.copyStaticFiles(); 31 | this.generateAppRoute(); 32 | this.generateExtensionRegistration(); 33 | this.generateModalFiles(); 34 | this.generateWidgetFiles(); 35 | this.configureBabel(); 36 | } 37 | 38 | copyStaticFiles () { 39 | this.fs.copyTpl( 40 | this.templatePath(`${this.templatesFolder}/web/**/*`), 41 | this.destinationPath(this.options.extensionOptions.webSrcFolder), 42 | this.templateProps 43 | ); 44 | 45 | this.fs.copyTpl( 46 | this.templatePath(`${this.templatesFolder}/hooks/*`), 47 | this.destinationPath('./hooks') 48 | ) 49 | } 50 | 51 | generateAppRoute () { 52 | this.fs.copyTpl( 53 | this.templatePath(`${this.templatesFolder}/app.ejs`), 54 | this.destinationPath(`${this.options.extensionOptions.webSrcFolder}/src/components/App.js`), 55 | this.templateProps 56 | ); 57 | } 58 | 59 | generateExtensionRegistration () { 60 | this.fs.copyTpl( 61 | this.templatePath(`${this.templatesFolder}/extension-registration.ejs`), 62 | this.destinationPath( 63 | `${this.options.extensionOptions.webSrcFolder}/src/components/ExtensionRegistration.js` 64 | ), 65 | this.templateProps 66 | ); 67 | } 68 | 69 | generateModalFiles () { 70 | const customButtons = this.options.extensionOptions.manifest.headerMenuButtons || []; 71 | 72 | customButtons.forEach((button) => { 73 | if (button.needsModal) { 74 | const fileName = button.id.replace(/-/g, '') + 'Modal'; 75 | const capitalizedFileName = fileName.charAt(0).toUpperCase() + fileName.slice(1); 76 | 77 | this.fs.copyTpl( 78 | this.templatePath(`${this.templatesFolder}/modal.ejs`), 79 | this.destinationPath( 80 | `${this.options.extensionOptions.webSrcFolder}/src/components/${capitalizedFileName}.js` 81 | ), 82 | { 83 | ...this.templateProps, 84 | button, 85 | } 86 | ); 87 | } 88 | }); 89 | } 90 | 91 | generateWidgetFiles () { 92 | const widgets = this.options.extensionOptions.manifest.rte?.widgets || []; 93 | 94 | widgets.forEach((widget) => { 95 | const fileName = widget.id.replace(/-/g, '') + 'Widget'; 96 | const capitalizedFileName = fileName.charAt(0).toUpperCase() + fileName.slice(1); 97 | 98 | this.fs.copyTpl( 99 | this.templatePath(`${this.templatesFolder}/widget.ejs`), 100 | this.destinationPath( 101 | `${this.options.extensionOptions.webSrcFolder}/src/components/${capitalizedFileName}.js` 102 | ), 103 | { 104 | ...this.templateProps, 105 | widget, 106 | } 107 | ); 108 | }); 109 | } 110 | 111 | configureBabel () { 112 | // NOTE this is a global file and might conflict 113 | this.fs.writeJSON(this.destinationPath('.babelrc'), { 114 | plugins: ['@babel/plugin-transform-react-jsx'], 115 | }); 116 | } 117 | } 118 | 119 | module.exports = WebAssetsReactGenerator; 120 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License") 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const Generator = require('yeoman-generator'); 13 | const path = require('path'); 14 | const chalk = require('chalk'); 15 | const { utils } = require('@adobe/generator-app-common-lib'); 16 | const { readManifest, writeManifest } = require('./utils'); 17 | const { briefOverviews, promptTopLevelFields, promptMainMenu, promptDocs } = require('./prompts'); 18 | 19 | const ConfigGenerator = require('./generators/config'); 20 | const DependenciesGenerator = require('./generators/dependencies'); 21 | const WebAssetsReactGenerator = require('./generators/web-assets-react'); 22 | // generate some additional things related only to demo extension 23 | const DemoExtensionGenerator = require('./generators/demo-extension'); 24 | 25 | const EXTENSION_MANIFEST_PATH = path.join(process.cwd(), 'extension-manifest.json'); 26 | // Don't put it in the manifest to avoid binding the generation "result" to the knowledge of "how it was generated" 27 | const DEMO_EXTENSION_TEMPLATES_FOLDER = 'chatgpt-demo'; 28 | 29 | /** 30 | * Possible hooks: 31 | * 32 | * initializing 33 | * prompting 34 | * configuring 35 | * default 36 | * writing 37 | * conflicts 38 | * install 39 | * end 40 | * 41 | */ 42 | class MainGenerator extends Generator { 43 | extensionOptions = {}; 44 | 45 | constructor (args, opts) { 46 | super(args, opts); 47 | 48 | this.option('skip-prompt', { default: false }); 49 | this.option('extension-manifest', { type: Object, default: undefined }); 50 | } 51 | 52 | initializing () { 53 | const extensionRootFolder = 'src/aem-cf-editor-1'; 54 | this.extensionOptions = { 55 | type: 'aem/cf-editor/1', 56 | rootFolder: extensionRootFolder, 57 | webSrcFolder: `${extensionRootFolder}/web-src`, 58 | configPath: `${extensionRootFolder}/ext.config.yaml`, 59 | manifest: this.options['extension-manifest'] 60 | ? this.options['extension-manifest'] 61 | : readManifest(EXTENSION_MANIFEST_PATH), 62 | demoExtensionTemplatesFolder: DEMO_EXTENSION_TEMPLATES_FOLDER, 63 | }; 64 | } 65 | 66 | async prompting () { 67 | if (this.options['skip-prompt']) { 68 | return; 69 | } 70 | this.log(briefOverviews.templateInfo); 71 | 72 | await promptTopLevelFields(this.extensionOptions.manifest) 73 | .then(() => promptMainMenu(this.extensionOptions.manifest)) 74 | .then(() => writeManifest(this.extensionOptions.manifest, EXTENSION_MANIFEST_PATH)) 75 | .then(() => { 76 | this.log('\nExtension Manifest for Code Pre-generation'); 77 | this.log('------------------------------------------'); 78 | this.log(JSON.stringify(this.extensionOptions.manifest, null, ' ')); 79 | }); 80 | } 81 | 82 | async writing () { 83 | this.composeWith( 84 | { 85 | Generator: WebAssetsReactGenerator, 86 | path: 'unknown', 87 | }, 88 | { 89 | extensionOptions: this.extensionOptions, 90 | } 91 | ); 92 | 93 | this.composeWith( 94 | { 95 | Generator: DependenciesGenerator, 96 | path: 'unknown', 97 | }, 98 | { 99 | extensionOptions: this.extensionOptions, 100 | } 101 | ); 102 | 103 | this.composeWith( 104 | { 105 | Generator: ConfigGenerator, 106 | path: 'unknown', 107 | }, 108 | { 109 | extensionOptions: this.extensionOptions, 110 | } 111 | ); 112 | 113 | if (this.extensionOptions.manifest.isDemoExtension) { 114 | // generate some additional things related only to demo extension 115 | this.composeWith( 116 | { 117 | Generator: DemoExtensionGenerator, 118 | path: 'unknown', 119 | }, 120 | { 121 | extensionOptions: this.extensionOptions, 122 | } 123 | ); 124 | } 125 | } 126 | 127 | async conflicts () { 128 | const content = utils.readPackageJson(this); 129 | content.description = this.extensionOptions.manifest.description; 130 | content.version = this.extensionOptions.manifest.version; 131 | utils.writePackageJson(this, content); 132 | } 133 | 134 | async end () { 135 | this.log(chalk.bold('\nSample code files have been generated.\n')); 136 | this.log(chalk.bold('Next Steps:')); 137 | this.log(chalk.bold('-----------')); 138 | this.log(chalk.bold('1) Populate your local environment variables in the ".env" file.')); 139 | this.log(chalk.bold('2) You can use `aio app run` or `aio app deploy` to see the sample code files in action.')); 140 | 141 | if (this.extensionOptions.manifest.isDemoExtension) { 142 | this.log(chalk.bold('3) Please refer to the link below for configuring the demo application:')); 143 | this.log(chalk.blue(chalk.bold(` -> ${promptDocs.demoExtensionDoc}`))); 144 | } 145 | this.log('\n'); 146 | } 147 | } 148 | 149 | module.exports = MainGenerator; 150 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const fs = require('fs-extra'); 13 | const { 14 | readManifest, 15 | writeManifest, 16 | generateRandomPrefix, 17 | generateUniqueWithinListIdFromValue, 18 | } = require('../src/utils'); 19 | 20 | describe('readManifest', () => { 21 | afterEach(() => { 22 | jest.resetAllMocks(); 23 | }); 24 | 25 | test('should return the parsed manifest file contents when the file exists', () => { 26 | const manifestPath = 'manifest.json'; 27 | const expectedManifest = { name: 'my-ext', version: '1.0.0' }; 28 | 29 | const readJsonSyncMock = jest.spyOn(fs, 'readJsonSync').mockReturnValue(expectedManifest); 30 | 31 | expect(readManifest(manifestPath)).toEqual(expectedManifest); 32 | expect(readJsonSyncMock).toHaveBeenCalledWith(manifestPath, { encoding: 'utf8' }); 33 | }); 34 | 35 | test('should return an empty object when the manifest file does not exist (ENOENT)', () => { 36 | const manifestPath = 'nonexistent-file.json'; 37 | const readJsonSyncMock = jest.spyOn(fs, 'readJsonSync').mockImplementation(() => { 38 | const err = new Error(); 39 | err.code = 'ENOENT'; 40 | throw err; 41 | }); 42 | 43 | expect(readManifest(manifestPath)).toEqual({}); 44 | expect(readJsonSyncMock).toHaveBeenCalledWith(manifestPath, { encoding: 'utf8' }); 45 | }); 46 | 47 | test('should throw an error when throws an error with code other than ENOENT', () => { 48 | const manifestPath = 'invalid-manifest.json'; 49 | const readJsonSyncMock = jest.spyOn(fs, 'readJsonSync').mockImplementation(() => { 50 | const err = new Error(); 51 | err.code = 'EACCES'; 52 | throw err; 53 | }); 54 | 55 | try { 56 | readManifest(manifestPath); 57 | } catch (err) { 58 | expect(err.code).toEqual('EACCES'); 59 | } 60 | expect(readJsonSyncMock).toHaveBeenCalledWith(manifestPath, { encoding: 'utf8' }); 61 | }); 62 | 63 | test('should throw an error when some other error occurs such as manifest file contains not valid JSON', () => { 64 | const manifestPath = 'invalid-manifest.json'; 65 | const readJsonSyncMock = jest.spyOn(fs, 'readJsonSync').mockImplementation(() => { 66 | const err = new Error(); 67 | throw err; 68 | }); 69 | 70 | try { 71 | readManifest(manifestPath); 72 | } catch (err) { 73 | expect(err).toBeInstanceOf(Error); 74 | } 75 | expect(readJsonSyncMock).toHaveBeenCalledWith(manifestPath, { encoding: 'utf8' }); 76 | }); 77 | }); 78 | 79 | describe('writeManifest', () => { 80 | afterEach(() => { 81 | jest.resetAllMocks(); 82 | }); 83 | 84 | test('should write the manifest to file', () => { 85 | const manifestPath = 'manifest.json'; 86 | const manifest = { name: 'my-ext', version: '1.0.0' }; 87 | const writeJsonSyncMock = jest.spyOn(fs, 'writeJsonSync').mockImplementation(); 88 | 89 | writeManifest(manifest, manifestPath); 90 | 91 | expect(writeJsonSyncMock).toHaveBeenCalledWith(manifestPath, manifest, { spaces: 2 }); 92 | }); 93 | }); 94 | 95 | describe('generateRandomPrefix', () => { 96 | it('should generate a random prefix of the specified length', () => { 97 | const length = 5; 98 | const prefix = generateRandomPrefix(length); 99 | expect(prefix).toMatch(/[a-z]{5}/); 100 | }); 101 | }); 102 | 103 | describe('generateUniqueWithinListIdFromValue', () => { 104 | const list = [ 105 | { id: 'abc123' }, 106 | ]; 107 | 108 | it('should generate a unique ID from a value', () => { 109 | const value = 'My Example @# Value 1'; 110 | const id = generateUniqueWithinListIdFromValue(value, list); 111 | expect(id).toBe('my-example-value-1'); 112 | }); 113 | 114 | it('should add a random prefix if the ID does not contain alphabetic characters', () => { 115 | const value = '12345'; 116 | const id = generateUniqueWithinListIdFromValue(value, list); 117 | expect(id).toMatch(/^[a-z]{5}-12345$/); 118 | }); 119 | 120 | it('should add a random prefix if the ID starts with a numeric value', () => { 121 | const value = '7th My Example'; 122 | const id = generateUniqueWithinListIdFromValue(value, list); 123 | expect(id).toMatch(/^[a-z]{5}-7th-my-example$/); 124 | }); 125 | 126 | it('should add a random prefix if the ID already exists in the list', () => { 127 | const value = 'abc123'; 128 | const id = generateUniqueWithinListIdFromValue(value, list); 129 | expect(id).toMatch(/^[a-z]{5}-abc123$/); 130 | }); 131 | 132 | it('should re-generate ID if the initial value starts with a numeric and the generated value already exists in the list', () => { 133 | const value = '123 Example Value'; // start with numeric, should leads to adding of generated prefix 134 | const list = [ 135 | { id: 'abc123' }, 136 | ]; 137 | 138 | list.find = jest.fn() 139 | // emulate that value with the generated prefix already exists in the list 140 | .mockReturnValueOnce({ id: 'ntosn-123-example-value' }) 141 | .mockReturnValue(undefined); 142 | 143 | const id = generateUniqueWithinListIdFromValue(value, list); 144 | expect(id).toMatch(/^[[a-z]{5}-[a-z]{5}-123-example-value$/); 145 | }); 146 | 147 | it('should re-generate ID if generated value already exists in the list', () => { 148 | const value = 'abc123'; 149 | const list = [ 150 | { id: 'abc123' }, 151 | ]; 152 | 153 | list.find = jest.fn() 154 | // emulate that initial value already exists in the list, should leads to adding of generated prefix 155 | .mockReturnValueOnce({ id: 'ntosn-123-example-value' }) 156 | // emulate that value with the generated prefix already exists in the list 157 | .mockReturnValueOnce({ id: 'qwerty-123-example-value' }) 158 | .mockReturnValue(undefined); 159 | 160 | const id = generateUniqueWithinListIdFromValue(value, list); 161 | expect(id).toMatch(/^[a-z]{5}-[a-z]{5}-abc123$/); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /src/prompts.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License") 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const inquirer = require('inquirer'); 13 | const slugify = require('slugify'); 14 | const chalk = require('chalk'); 15 | const path = require('path'); 16 | const { readManifest } = require('./utils'); 17 | const headerMenuButtonPrompts = require('./prompts/header-menu'); 18 | const { toolbarButtonPrompts, badgePrompts, widgetPrompts } = require('./prompts/rte'); 19 | 20 | const DEMO_MANIFEST_PATH = path.join(__dirname, './manifests/demo-extension-manifest.json'); 21 | 22 | let exitMenu = false; 23 | 24 | const briefOverviews = { 25 | templateInfo: `\nAEM Content Fragment Editor Template Overview:\n 26 | * You have the option to generate boilerplate code for your extension. 27 | * You can get help regarding documentation at any time from the menu. 28 | * You can check out a sample demo project. 29 | * An App Builder project will be created with Node.js packages pre-configured.\n`, 30 | }; 31 | 32 | const promptDocs = { 33 | mainDoc: 'https://developer.adobe.com/uix/docs/', 34 | demoExtensionDoc: 'https://developer.adobe.com/uix/docs/', 35 | }; 36 | 37 | // Top Level prompts 38 | const promptTopLevelFields = (manifest) => { 39 | return inquirer.prompt([ 40 | { 41 | type: 'input', 42 | name: 'name', 43 | message: 'What do you want to name your extension?', 44 | validate (answer) { 45 | if (!answer.length) { 46 | return 'Required.'; 47 | } 48 | 49 | return true; 50 | }, 51 | }, 52 | { 53 | type: 'input', 54 | name: 'description', 55 | message: 'Please provide a short description of your extension:', 56 | validate (answer) { 57 | if (!answer.length) { 58 | return 'Required.'; 59 | } 60 | 61 | return true; 62 | }, 63 | }, 64 | { 65 | type: 'input', 66 | name: 'version', 67 | message: 'What version would you like to start with?', 68 | default: '0.0.1', 69 | validate(answer) { 70 | if (!new RegExp("^\\bv?(?:0|[1-9][0-9]*)(?:\\.(?:0|[1-9][0-9]*)){2}(?:-[\\da-z\\-]+(?:\\.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:\\.[\\da-z\\-]+)*)?\\b$").test(answer)) { 71 | return 'Required. Must match semantic versioning rules.' 72 | } 73 | 74 | return true; 75 | }, 76 | }, 77 | ]) 78 | .then((answers) => { 79 | if (answers.name) { 80 | manifest.name = answers.name; 81 | manifest.id = slugify(answers.name, { 82 | lower: true, 83 | strict: true, 84 | }); 85 | } 86 | 87 | if (answers.description) { 88 | manifest.description = answers.description; 89 | } 90 | 91 | if (answers.version) { 92 | manifest.version = answers.version; 93 | } 94 | }); 95 | }; 96 | 97 | // Main Menu prompts 98 | const promptMainMenu = (manifest) => { 99 | const choices = []; 100 | 101 | choices.push( 102 | new inquirer.Separator(), 103 | { 104 | name: 'Add a custom button to Header Menu', 105 | value: headerMenuButtonPrompts.bind(this, manifest), 106 | }, 107 | { 108 | name: 'Add Rich Text Editor (RTE) Toolbar Button', 109 | value: toolbarButtonPrompts.bind(this, manifest), 110 | }, 111 | { 112 | name: 'Add Rich Text Editor (RTE) Toolbar Widget', 113 | value: widgetPrompts.bind(this, manifest), 114 | }, 115 | { 116 | name: 'Add Rich Text Editor (RTE) Toolbar Badge', 117 | value: badgePrompts.bind(this, manifest), 118 | }, 119 | new inquirer.Separator(), 120 | { 121 | name: "I'm done", 122 | value: () => { 123 | return Promise.resolve(true); 124 | }, 125 | }, 126 | { 127 | name: "I don't know", 128 | value: promptGuideMenu.bind(this, manifest), 129 | } 130 | ); 131 | 132 | return inquirer 133 | .prompt({ 134 | type: 'list', 135 | name: 'execute', 136 | message: 'What would you like to do next?', 137 | choices, 138 | }) 139 | .then((answers) => answers.execute()) 140 | .then((endMainMenu) => { 141 | if (!endMainMenu && !exitMenu) { 142 | return promptMainMenu(manifest); 143 | } 144 | }) 145 | .catch((error) => { 146 | console.log(error); 147 | }); 148 | }; 149 | 150 | // Guide Menu Prompts 151 | const promptGuideMenu = (manifest) => { 152 | const choices = []; 153 | 154 | choices.push( 155 | new inquirer.Separator(), 156 | { 157 | name: 'Try a demo project based on React (ChatGPT support for Rich Text Editor)', 158 | value: () => { 159 | const demoManifest = readManifest(DEMO_MANIFEST_PATH); 160 | 161 | // Update the extension manifest object 162 | manifest.name = demoManifest.name || null; 163 | manifest.id = demoManifest.id || null; 164 | manifest.description = demoManifest.description || null; 165 | manifest.version = demoManifest.version || null; 166 | manifest.templateFolder = demoManifest.templateFolder || null; 167 | manifest.templateDotEnvVars = demoManifest.templateDotEnvVars || null; 168 | manifest.isDemoExtension = demoManifest.isDemoExtension || false; 169 | exitMenu = true; 170 | 171 | return Promise.resolve(true); 172 | }, 173 | }, 174 | { 175 | name: 'Find some help', 176 | value: helpPrompts.bind(this), 177 | }, 178 | new inquirer.Separator(), 179 | { 180 | name: 'Go back', 181 | value: () => { 182 | return Promise.resolve(true); 183 | }, 184 | } 185 | ); 186 | 187 | return inquirer 188 | .prompt({ 189 | type: 'list', 190 | name: 'execute', 191 | message: 'What about this then?', 192 | choices, 193 | }) 194 | .then((answers) => answers.execute()) 195 | .then((endGuideMenu) => { 196 | if (!endGuideMenu) { 197 | return promptGuideMenu(manifest); 198 | } 199 | }) 200 | .catch((error) => { 201 | console.log(error); 202 | }); 203 | }; 204 | 205 | // Helper prompts for Guide Menu 206 | const helpPrompts = () => { 207 | console.log(' Please refer to:'); 208 | console.log(chalk.blue(chalk.bold(` -> ${promptDocs.mainDoc}`)) + '\n'); 209 | }; 210 | 211 | module.exports = { 212 | briefOverviews, 213 | promptTopLevelFields, 214 | promptMainMenu, 215 | promptDocs, 216 | }; 217 | -------------------------------------------------------------------------------- /src/templates/chatgpt-demo/rte-toolbar-buttons.ejs: -------------------------------------------------------------------------------- 1 | // DEMO EXTENSION BUTTONS DECLARATION 2 | { 3 | id: "my.company.generate", 4 | tooltip: "Generate AI text based on the prompt. Highlight the text and select", 5 | icon: 'MagicWand', 6 | toolbarGroup: 8, 7 | onClick: async (state) => { 8 | try { 9 | checkState(state); 10 | const result = await makeRequest(state.selectedText); 11 | 12 | return [{ 13 | type: "replaceContent", 14 | value: state.text.replace(state.selectedText, result) 15 | }] 16 | 17 | } catch (e) { 18 | await guestConnection.host.toaster.display({ variant: "negative", message: e.message}); 19 | console.error('[CHATGPT EXTENSION] Error happened in extension', e); 20 | return []; 21 | } 22 | } 23 | }, 24 | { 25 | id: "my.company.complete", 26 | tooltip: "Let AI pickup where you left off. Start writing, highlight the text, and select", 27 | icon: 'Type', 28 | toolbarGroup: 8, 29 | onClick: async (state) => { 30 | try { 31 | checkState(state); 32 | const result = await makeRequest("Complete a following text:\n" + state.selectedText); 33 | 34 | return [{ 35 | type: "replaceContent", 36 | value: state.text.replace(state.selectedText, state.selectedText + result) 37 | }] 38 | } catch (e) { 39 | await guestConnection.host.toaster.display({ variant: "negative", message: e.message}); 40 | console.error('[CHATGPT EXTENSION] Error happened in extension', e); 41 | return []; 42 | } 43 | } 44 | }, 45 | { 46 | id: "my.company.professional", 47 | tooltip: "Rewrite text in a professional tone. Highlight text and select", 48 | icon: 'AnnotatePen', 49 | toolbarGroup: 9, 50 | onClick: async (state) => { 51 | try { 52 | checkState(state); 53 | const result = await makeRequest("Rewrite a text below to be business writing:\n" + state.selectedText); 54 | 55 | return [{ 56 | type: "replaceContent", 57 | value: state.text.replace(state.selectedText, result) 58 | }] 59 | } catch (e) { 60 | await guestConnection.host.toaster.display({ variant: "negative", message: e.message}); 61 | console.error('[CHATGPT EXTENSION] Error happened in extension', e); 62 | return []; 63 | } 64 | } 65 | }, 66 | { 67 | id: "my.company.persuasive", 68 | tooltip: "Rewrite text in a persuasive tone. Highlight text and select", 69 | icon: 'Promote', 70 | toolbarGroup: 9, 71 | onClick: async (state) => { 72 | try { 73 | checkState(state); 74 | const result = await makeRequest("Rewrite to be persuasive:\n" + state.selectedText); 75 | 76 | return [{ 77 | type: "replaceContent", 78 | value: state.text.replace(state.selectedText, result) 79 | }] 80 | } catch (e) { 81 | await guestConnection.host.toaster.display({ variant: "negative", message: e.message}); 82 | console.error('[CHATGPT EXTENSION] Error happened in extension', e); 83 | return []; 84 | } 85 | } 86 | }, 87 | { 88 | id: "my.company.engaging", 89 | tooltip: "Rewrite text in an engaging tone to go viral. Highlight text and select", 90 | icon: 'SocialNetwork', 91 | toolbarGroup: 9, 92 | onClick: async (state) => { 93 | try { 94 | checkState(state); 95 | const result = await makeRequest("Rewrite the text below to be engaging and go viral for social media:\n" + state.selectedText); 96 | 97 | return [{ 98 | type: "replaceContent", 99 | value: state.text.replace(state.selectedText, result) 100 | }] 101 | } catch (e) { 102 | await guestConnection.host.toaster.display({ variant: "negative", message: e.message}); 103 | console.error('[CHATGPT EXTENSION] Error happened in extension', e); 104 | return []; 105 | } 106 | } 107 | }, 108 | { 109 | id: "my.company.simplify", 110 | tooltip: "Translates text into simpler concepts. Highlight text and select", 111 | icon: 'GraphBarHorizontal', 112 | toolbarGroup: 9, 113 | onClick: async (state) => { 114 | try { 115 | checkState(state); 116 | const result = await makeRequest("Simplify a following text:\n" + state.selectedText); 117 | 118 | return [{ 119 | type: "replaceContent", 120 | value: state.text.replace(state.selectedText, result) 121 | }] 122 | } catch (e) { 123 | await guestConnection.host.toaster.display({ variant: "negative", message: e.message}); 124 | console.error('[CHATGPT EXTENSION] Error happened in extension', e); 125 | return []; 126 | } 127 | } 128 | }, 129 | { 130 | id: "my.company.summarize", 131 | tooltip: "Summarize the text using AI. Highlight text and select", 132 | icon: 'Summarize', 133 | toolbarGroup: 9, 134 | onClick: async (state) => { 135 | try { 136 | checkState(state); 137 | const result = await makeRequest(state.selectedText + "\n tl;dr \n"); 138 | 139 | return [{ 140 | type: "replaceContent", 141 | value: state.text.replace(state.selectedText, result) 142 | }] 143 | } catch (e) { 144 | await guestConnection.host.toaster.display({ variant: "negative", message: e.message}); 145 | console.error('[CHATGPT EXTENSION] Error happened in extension', e); 146 | return []; 147 | } 148 | } 149 | }, 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------