├── src ├── templates │ ├── web │ │ ├── src │ │ │ ├── config.json │ │ │ ├── index.css │ │ │ ├── components │ │ │ │ └── Constants.js │ │ │ ├── index.js │ │ │ └── utils.js │ │ └── index.html │ ├── _shared │ │ ├── stub-modal.ejs │ │ ├── stub-app.ejs │ │ └── stub-extension-registration.ejs │ └── hooks │ │ └── post-deploy.js ├── utils.js ├── generator-add-action.js ├── generator-add-web-assets.js ├── index.js └── prompts.js ├── COPYRIGHT ├── install.yml ├── .gitignore ├── .github └── workflows │ └── on-release-publish-to-npm.yml ├── jest.config.js ├── package.json ├── test ├── e2e.js ├── jest.setup.js ├── test-manifests.js ├── utils.test.js ├── generator-add-action.test.js ├── index.test.js ├── generator-add-web-assets.test.js └── prompts.test.js ├── README.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── LICENSE /src/templates/web/src/config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/templates/web/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/templates/web/src/components/Constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | export const extensionId = '<%- extensionManifest.id %>'; 6 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2025 Adobe. All rights reserved. 2 | 3 | Adobe holds the copyright for all the files found in this repository. 4 | 5 | See the LICENSE file for licensing information. -------------------------------------------------------------------------------- /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/assets/browse/1 10 | -------------------------------------------------------------------------------- /src/templates/web/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import App from './components/App'; 7 | import './index.css'; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # package directories 3 | node_modules 4 | 5 | # Dependency Management 6 | package-lock.json 7 | 8 | .env* 9 | .aio 10 | 11 | # Test output 12 | junit.xml 13 | 14 | # IDE & Temp 15 | .cache 16 | .idea 17 | .iml 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 | 31 | # this template's e2e test directory 32 | temp-template-test -------------------------------------------------------------------------------- /src/templates/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- extensionManifest.name %> 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.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@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 18 14 | - run: npm install 15 | - uses: JS-DevTools/npm-publish@v3 16 | with: 17 | token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 18 | access: "public" -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 | module.exports = { 13 | testEnvironment: 'node', 14 | verbose: true, 15 | setupFilesAfterEnv: ['./test/jest.setup.js'], 16 | collectCoverage: true, 17 | collectCoverageFrom: [], 18 | testPathIgnorePatterns: [ 19 | "/node_modules/" 20 | ], 21 | coverageThreshold: { 22 | global: { 23 | branches: 40, 24 | lines: 70, 25 | statements: 70 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 | 14 | function readManifest(manifestPath) { 15 | try { 16 | return JSON.parse( 17 | fs.readFileSync(manifestPath, {encoding: 'utf8'}) 18 | ) 19 | } catch (err) { 20 | if (err.code === 'ENOENT') { 21 | return {} 22 | } else { 23 | throw err 24 | } 25 | } 26 | } 27 | 28 | function writeManifest(manifest, manifestPath) { 29 | fs.writeJsonSync(manifestPath, manifest, {spaces: 2}) 30 | } 31 | 32 | module.exports = { 33 | readManifest, 34 | writeManifest 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-assets-browse-ext-tpl", 3 | "version": "0.0.3", 4 | "main": "src/index.js", 5 | "description": "Asset Browse extension template for the AEM Assets View", 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/generator-app-common-lib": "^2.0.0", 16 | "chalk": "^4.0.0", 17 | "fs-extra": "^11.0.0", 18 | "slugify": "^1.6.5", 19 | "yeoman-generator": "^5" 20 | }, 21 | "devDependencies": { 22 | "jest": "^29.0.0", 23 | "stdout-stderr": "^0.1.13", 24 | "yeoman-test": "^6.3.0", 25 | "js-yaml": "^4.1.0", 26 | "yeoman-assert": "^3.1.1", 27 | "yeoman-environment": "^3.2.0", 28 | "lodash.clonedeep": "^4.5.0", 29 | "@adobe/aio-lib-template-validation": "^5.0.0" 30 | }, 31 | "peerDependencies": { 32 | "@adobe/aio-cli-plugin-app-templates": "^1.0.0 || ^2.0.0" 33 | }, 34 | "scripts": { 35 | "validate": "tv run-checks .", 36 | "test": "npm run unit-tests && npm run validate", 37 | "unit-tests": "jest ./test", 38 | "e2e": "node test/e2e.js" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/e2e.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 yeoman = require('yeoman-environment') 13 | const path = require('path') 14 | const fs = require('fs-extra') 15 | const mainGenerator = require('../src/index') 16 | 17 | async function main () { 18 | const targetFolder = path.join(process.cwd(), 'temp-template-test') 19 | 20 | fs.ensureDirSync(targetFolder) 21 | process.chdir(targetFolder) 22 | fs.ensureFileSync('.env') 23 | 24 | // TODO: add your options in here 25 | const options = {} 26 | 27 | const env = yeoman.createEnv() 28 | const gen = env.instantiate( 29 | mainGenerator, 30 | { 31 | options 32 | } 33 | ) 34 | 35 | await env.runGenerator(gen) 36 | } 37 | 38 | main() 39 | .catch(console.error) 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | # @adobe/aem-assets-browse-ext-tpl 14 | 15 | Template for an AIO CLI App Builder plugin that generates code for a UI extension in the Asset Browse section of the AEM Assets View 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 | # Contributing 29 | Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information. 30 | 31 | # Licensing 32 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. 33 | -------------------------------------------------------------------------------- /test/jest.setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 { stdout, stderr } = require('stdout-stderr'); 14 | 15 | process.on('unhandledRejection', error => { 16 | throw error; 17 | }); 18 | 19 | // trap console log 20 | beforeEach(() => { 21 | stdout.start(); 22 | stderr.start(); 23 | stdout.print = false; // set to true to see output 24 | }); 25 | 26 | afterEach(() => { 27 | stdout.stop(); 28 | stderr.stop(); 29 | }); 30 | 31 | // quick normalization to test windows/unix paths 32 | global.n = p => path.normalize(p); 33 | global.r = p => path.resolve(p); 34 | 35 | /** 36 | * Checks that package.json has all needed dependencies specified. 37 | * 38 | * @param {object} dependencies An object representing expected package.json dependencies. 39 | * @param {object} devDependencies An object representing expected package.json dev dependencies. 40 | */ 41 | global.assertDependencies = (fs, dependencies, devDependencies) => { 42 | expect(JSON.parse(fs.readFileSync('package.json').toString())).toEqual(expect.objectContaining({ 43 | dependencies, 44 | devDependencies 45 | })); 46 | }; 47 | -------------------------------------------------------------------------------- /src/templates/_shared/stub-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 | defaultTheme, 11 | Link, 12 | Text, 13 | ButtonGroup, 14 | Button, 15 | View 16 | } from '@adobe/react-spectrum'; 17 | 18 | import { extensionId } from './Constants'; 19 | 20 | export default function <%- modalComponentName %>() { 21 | // Fields 22 | const [guestConnection, setGuestConnection] = useState(); 23 | const [colorScheme, setColorScheme] = useState('light'); 24 | 25 | useEffect(() => { 26 | (async () => { 27 | const guestConnection = await attach({ id: extensionId }); 28 | setGuestConnection(guestConnection); 29 | 30 | const { colorScheme } = await guestConnection.host.theme.getThemeInfo(); 31 | setColorScheme(colorScheme); 32 | })() 33 | }, []); 34 | 35 | function closeDialog() { 36 | guestConnection.host.modal.closeDialog(); 37 | } 38 | 39 | return ( 40 | <% if (modalType === 'fullscreen') { %> 41 | 42 | <% } else { %> 43 | 44 | <% } %> 45 | 46 | 47 | Please visit UI Extensibility documentation to get started. 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/templates/hooks/post-deploy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 chalk = require('chalk'); 13 | const fs = require('fs'); 14 | const yaml = require('js-yaml'); 15 | 16 | module.exports = (config) => { 17 | try { 18 | // read the app.config.yaml file to get the extension points 19 | const yamlFile = fs.readFileSync(`${config.root}/app.config.yaml`, 'utf8'); 20 | const yamlData = yaml.load(yamlFile); 21 | const { extensions } = yamlData; 22 | 23 | // For now we are ok just to read the first extension point to build the preview link 24 | const extension = Object.keys(extensions)[0]; 25 | const previewData = { 26 | extensionPoint: extension, 27 | url: config.project.workspace.app_url, 28 | }; 29 | 30 | // build the preview URL 31 | const base64EncodedData = Buffer.from(JSON.stringify(previewData)).toString('base64'); 32 | console.log(chalk.magenta(chalk.bold('For a developer preview of your UI extension in the AEM Assets View environment, follow the URL:'))); 33 | 34 | // check if the environment is stage, if so, we need to add the -stage suffix to the URL 35 | const env = process.env.AIO_CLI_ENV === 'stage' ? '-stage' : ''; 36 | console.log(chalk.magenta(chalk.bold(` -> https://experience${env}.adobe.com/aem/extension-manager/preview/${base64EncodedData}`))); 37 | } catch (error) { 38 | // if something went wrong, we do nothing, and just don't display the URL 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /test/test-manifests.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 defaultExtensionManifest = { 13 | "name": "AEM Assets View ActionBar Test Extension", 14 | "id": "aem-assets-browse-test-extension", 15 | "description": "Test Extension for AEM Assets View ActionBar", 16 | "version": "0.0.1" 17 | } 18 | 19 | const customExtensionManifest = { 20 | "name": "AEM Assets View ActionBar Test Extension", 21 | "id": "aem-assets-browse-test-extension", 22 | "description": "Test Extension for AEM Assets View ActionBar", 23 | "version": "0.0.1", 24 | "actionBarActions": [ 25 | { 26 | "id": "a1", 27 | "icon": "Attributes", 28 | "label": "Label 1" 29 | }, 30 | { 31 | "id": "a2", 32 | "icon": "Chat", 33 | "label": "Label 2" 34 | } 35 | ], 36 | "headerMenuButtons": [ 37 | { 38 | "id": "h1", 39 | "icon": "Settings", 40 | "label": "Header Button 1", 41 | "needsModal": true, 42 | "modalTitle": "Header Settings", 43 | "modalType": "modal", 44 | "modalSize": "M", 45 | "componentName": "Modalh1" 46 | }, 47 | { 48 | "id": "h2", 49 | "icon": "Help", 50 | "label": "Header Button 2" 51 | } 52 | ], 53 | "runtimeActions": [ 54 | { 55 | "name": "attributes" 56 | }, 57 | { 58 | "name": "chat" 59 | } 60 | ] 61 | } 62 | 63 | module.exports = { 64 | defaultExtensionManifest, 65 | customExtensionManifest, 66 | } -------------------------------------------------------------------------------- /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, 10 | you are expected to uphold this code. Please report unacceptable behavior to 11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 12 | 13 | ## Have A Question? 14 | 15 | Start by filing an issue. The existing committers on this project work to reach 16 | consensus around project direction and issue solutions within issue threads 17 | (when appropriate). 18 | 19 | ## Contributor License Agreement 20 | 21 | All third-party contributions to this project must be accompanied by a signed contributor 22 | license agreement. This gives Adobe permission to redistribute your contributions 23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 24 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 25 | you are good to go! 26 | 27 | ## Code Reviews 28 | 29 | All submissions should come in the form of pull requests and need to be reviewed 30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 31 | for more information on sending pull requests. 32 | 33 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when 34 | submitting a pull request! 35 | 36 | ## From Contributor To Committer 37 | 38 | We love contributions from our community! If you'd like to go a step beyond contributor 39 | and become a committer with full write access and a say in the project, you must 40 | be invited to the project. The existing committers employ an internal nomination 41 | process that must reach lazy consensus (silence is approval) before invitations 42 | are issued. If you feel you are qualified and want to get more deeply involved, 43 | feel free to reach out to existing committers to have a conversation about that. 44 | 45 | ## Security Issues 46 | 47 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html). -------------------------------------------------------------------------------- /src/templates/_shared/stub-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 | <%_ const actionBarActions = extensionManifest.actionBarActions || [] -%> 10 | <%_ actionBarActions.forEach((action) => { -%> 11 | <%_ if (action.needsModal) { -%> 12 | import <%- action.componentName %> from './<%- action.componentName %>'; 13 | <%_ } -%> 14 | <%_ }) -%> 15 | <%_ const headerMenuButtons = extensionManifest.headerMenuButtons || [] -%> 16 | <%_ headerMenuButtons.forEach((button) => { -%> 17 | <%_ if (button.needsModal) { -%> 18 | import <%- button.componentName %> from './<%- button.componentName %>'; 19 | <%_ } -%> 20 | <%_ }) -%> 21 | 22 | function App() { 23 | return ( 24 | 25 | 26 | 27 | } /> 28 | } /> 29 | <%_ if (extensionManifest.actionBarActions) { -%> 30 | <%_ extensionManifest.actionBarActions.forEach((action) => { -%> 31 | <%_ if (action.needsModal) { -%> 32 | />} /> 33 | <%_ } -%> 34 | <%_ })} -%> 35 | <%_ if (extensionManifest.headerMenuButtons) { -%> 36 | <%_ extensionManifest.headerMenuButtons.forEach((button) => { -%> 37 | <%_ if (button.needsModal) { -%> 38 | />} /> 39 | <%_ } -%> 40 | <%_ })} -%> 41 | // YOUR CUSTOM ROUTES SHOULD BE HERE 42 | 43 | 44 | 45 | ); 46 | 47 | // Methods 48 | 49 | // error handler on UI rendering failure 50 | function onError(e, componentStack) {} 51 | 52 | // component to show if UI fails rendering 53 | function fallbackComponent({ componentStack, error }) { 54 | return ( 55 | 56 |

57 | Extension rendering error 58 |

59 |
{componentStack + '\n' + error.message}
60 |
61 | ); 62 | } 63 | } 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 { readManifest, writeManifest } = require('../src/utils'); 14 | const { defaultExtensionManifest, customExtensionManifest } = require('./test-manifests'); 15 | 16 | jest.mock('fs'); 17 | 18 | 19 | describe('readManifest', () => { 20 | beforeEach(() => { 21 | jest.clearAllMocks(); 22 | }); 23 | 24 | it.each([ 25 | ['default', defaultExtensionManifest], 26 | ['custom', customExtensionManifest ], 27 | ])('should read and parse %s manifest file', async (desc, json) => { 28 | const mockPath = 'path/to/manifest.yml'; 29 | 30 | fs.readFileSync.mockReturnValue( 31 | JSON.stringify(json) 32 | ); 33 | 34 | const result = readManifest(mockPath); 35 | 36 | expect(fs.readFileSync).toHaveBeenCalledWith(mockPath, {"encoding": "utf8"}); 37 | expect(result).toMatchObject(json); 38 | }); 39 | 40 | it('should throw error if file cannot be read', () => { 41 | const mockPath = 'invalid/path'; 42 | fs.readFileSync.mockImplementation(() => { 43 | throw new Error('File not found'); 44 | }); 45 | 46 | expect(() => readManifest(mockPath)).toThrow('File not found'); 47 | }); 48 | 49 | it('should return empty JSON if file is empty', () => { 50 | const mockPath = 'invalid/path'; 51 | fs.readFileSync.mockImplementation(() => { 52 | const error = new Error('ENOENT'); 53 | error.code = 'ENOENT'; 54 | throw error; 55 | }); 56 | 57 | const result = readManifest(mockPath); 58 | expect(result).toMatchObject({}); 59 | }); 60 | }); 61 | 62 | describe('writeManifest', () => { 63 | beforeEach(() => { 64 | jest.clearAllMocks(); 65 | }); 66 | 67 | it.each([ 68 | ['default', defaultExtensionManifest], 69 | ['custom', customExtensionManifest ], 70 | ])('should write %s manifest to file', (desc, json) => { 71 | const mockPath = 'path/to/manifest.yml'; 72 | 73 | fs.writeJsonSync = jest.fn(); 74 | 75 | writeManifest(json, mockPath); 76 | 77 | expect(fs.writeJsonSync).toHaveBeenCalledWith(mockPath, json, {"spaces": 2}); 78 | }); 79 | 80 | it('should throw error if file cannot be written', () => { 81 | const mockPath = 'invalid/path'; 82 | const mockYaml = { name: 'test' }; 83 | 84 | fs.writeJsonSync.mockImplementation(() => { 85 | throw new Error('Cannot write file'); 86 | }); 87 | 88 | expect(() => writeManifest(mockPath, defaultExtensionManifest)).toThrow('Cannot write file'); 89 | }); 90 | }); 91 | 92 | -------------------------------------------------------------------------------- /src/generator-add-action.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 { ActionGenerator, commonTemplates } = require('@adobe/generator-app-common-lib'); 14 | 15 | class ExtensionActionGenerator extends ActionGenerator { 16 | constructor(args, opts) { 17 | super(args, opts); 18 | this.props = { 19 | description: 'This is a sample action showcasing how to access an external API', 20 | // eslint-disable-next-line quotes 21 | requiredParams: `[/* add required params */]`, 22 | // eslint-disable-next-line quotes 23 | requiredHeaders: `['Authorization']`, 24 | // eslint-disable-next-line quotes 25 | importCode: `const fetch = require('node-fetch') 26 | const { Core } = require('@adobe/aio-sdk')`, 27 | 28 | responseCode: `// replace this with the api you want to access 29 | const apiEndpoint = \`\${params.API_ENDPOINT}\` 30 | // fetch content from external api endpoint 31 | const res = await fetch(apiEndpoint) 32 | if (!res.ok) { 33 | throw new Error('request to ' + apiEndpoint + ' failed with status code ' + res.status) 34 | } 35 | const content = await res.json() 36 | const response = { 37 | statusCode: 200, 38 | body: content 39 | }` 40 | }; 41 | 42 | this.props['actionName'] = this.options['action-name']; 43 | this.props['extensionManifest'] = this.options['extension-manifest']; 44 | } 45 | 46 | writing() { 47 | this.sourceRoot(path.join(__dirname, '.')); 48 | 49 | // Generic Project 50 | var templateActionPath = commonTemplates['stub-action']; 51 | var templateInputs = { 52 | LOG_LEVEL: 'debug', 53 | API_ENDPOINT: '$API_ENDPOINT' 54 | }; 55 | var templateDotEnvVars = ['API_ENDPOINT']; 56 | 57 | this.addAction(this.props.actionName, templateActionPath, { 58 | sharedLibFile: commonTemplates['utils'], 59 | sharedLibTestFile: commonTemplates['utils.test'], 60 | e2eTestFile: commonTemplates['stub-action.e2e'], 61 | tplContext: this.props, 62 | dependencies: { 63 | 'node-fetch': '^2.6.0' 64 | }, 65 | actionManifestConfig: { 66 | inputs: templateInputs, 67 | annotations: { 68 | 'final': true, 69 | 'require-adobe-auth': false 70 | } // makes sure loglevel cannot be overwritten by request param 71 | }, 72 | dotenvStub: {label: 'Place your local environment variables here', vars: templateDotEnvVars} 73 | }); 74 | } 75 | } 76 | 77 | module.exports = ExtensionActionGenerator; 78 | -------------------------------------------------------------------------------- /src/templates/_shared/stub-extension-registration.ejs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | import React from 'react'; 6 | import { Text } from '@adobe/react-spectrum'; 7 | import { register } from '@adobe/uix-guest'; 8 | import { extensionId } from './Constants'; 9 | 10 | function ExtensionRegistration() { 11 | const init = async () => { 12 | const guestConnection = await register({ 13 | id: extensionId, 14 | methods: { 15 | actionBar: { 16 | async getActions({ context, resourceSelection }) { 17 | // YOUR ACTION BAR ACTIONS CODE SHOULD BE HERE 18 | return [ 19 | <%_ if (extensionManifest.actionBarActions) { -%> 20 | <%_ extensionManifest.actionBarActions.forEach((action) => { -%> 21 | { 22 | 'id': '<%- action.id %>', 23 | 'icon': '<%- action.icon %>', 24 | 'label': '<%- action.label %>', 25 | 'onClick': async () => { 26 | <%_ if (action.needsModal) { -%> 27 | // openDialog: ({ title, contentUrl, type, size, payload }) => {}, 28 | guestConnection.host.modal.openDialog({ 29 | title: '<%- action.modalTitle %>', 30 | contentUrl: '/#modal-<%- action.id %>', 31 | type: '<%- action.modalType %>', 32 | size: '<%- action.modalSize %>', 33 | payload: { /* arbitrary payload */ } 34 | }); 35 | <%_ } else { -%> 36 | console.log('Action.id: ', '<%- action.id %>'); 37 | <%_ } -%> 38 | }, 39 | }, 40 | <%_ })} -%> 41 | ]; 42 | }, 43 | async getHiddenBuiltInActions({ context, resourceSelection }) { 44 | return []; 45 | }, 46 | async overrideBuiltInAction({ actionId, context, resourceSelection }) { 47 | // perform some custom tasks 48 | // override built-in action by return true; 49 | // return true; 50 | // or return false to continue with built-in action 51 | return false; 52 | }, 53 | }, 54 | quickActions: { 55 | async getHiddenBuiltInActions({ context, resource }) { 56 | return []; 57 | }, 58 | async overrideBuiltInAction({ actionId, context, resource }) { 59 | // perform some custom tasks 60 | // override built-in action by return true; 61 | // return true; 62 | // or return false to continue with built-in action 63 | return false; 64 | }, 65 | }, 66 | headerMenu: { 67 | async getButtons({ context, resourceSelection }) { 68 | // YOUR HEADER MENU BUTTONS SHOULD BE RETURNED IN THE ARRAY 69 | return [ 70 | <%_ if (extensionManifest.headerMenuButtons) { -%> 71 | <%_ extensionManifest.headerMenuButtons.forEach((button) => { -%> 72 | { 73 | 'id': '<%- button.id %>', 74 | 'icon': '<%- button.icon %>', 75 | 'label': '<%- button.label %>', 76 | 'onClick': async () => { 77 | <%_ if (button.needsModal) { -%> 78 | guestConnection.host.modal.openDialog({ 79 | title: '<%- button.modalTitle %>', 80 | contentUrl: '/#modal-<%- button.id %>', 81 | type: '<%- button.modalType %>', 82 | size: '<%- button.modalSize %>', 83 | payload: { /* arbitrary payload */ } 84 | }); 85 | <%_ } else { -%> 86 | console.log('Header Menu button.id: ', '<%- button.id %>'); 87 | <%_ } -%> 88 | }, 89 | }, 90 | <%_ })} -%> 91 | ]; 92 | }, 93 | }, 94 | }, 95 | }); 96 | }; 97 | init().catch(console.error); 98 | 99 | return IFrame for integration with Host (AEM Assets View)...; 100 | } 101 | 102 | export default ExtensionRegistration; 103 | -------------------------------------------------------------------------------- /test/generator-add-action.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 assert = require('yeoman-assert'); 14 | const cloneDeep = require('lodash.clonedeep'); 15 | 16 | const yaml = require('js-yaml'); 17 | const fs = require('fs'); 18 | const path = require('path'); 19 | 20 | const ExtensionActionGenerator = require('../src/generator-add-action'); 21 | 22 | const { ActionGenerator, constants } = require('@adobe/generator-app-common-lib'); 23 | const { runtimeManifestKey, defaultRuntimeKind } = constants; 24 | const { customExtensionManifest } = require('./test-manifests'); 25 | 26 | const extFolder = 'src/aem-assets-browse-1'; 27 | const actionFolder = path.join(extFolder, 'actions'); 28 | const extConfigPath = path.join(extFolder, 'ext.config.yaml'); 29 | const actionName = 'generic'; 30 | 31 | const basicGeneratorOptions = { 32 | 'action-folder': actionFolder, 33 | 'config-path': extConfigPath, 34 | 'full-key-to-manifest': runtimeManifestKey, 35 | 'action-name': actionName, 36 | 'extension-manifest': customExtensionManifest 37 | }; 38 | 39 | describe('prototype', () => { 40 | test('exports a yeoman generator', () => { 41 | expect(ExtensionActionGenerator.prototype).toBeInstanceOf(ActionGenerator); 42 | }) 43 | }); 44 | 45 | /** 46 | * Checks that all the files are generated. 47 | * 48 | * @param {string} actionName an action name 49 | */ 50 | function assertGeneratedFiles (actionName) { 51 | assert.file(basicGeneratorOptions['config-path']); 52 | 53 | assert.file(`${basicGeneratorOptions['action-folder']}/${actionName}/index.js`); 54 | 55 | // assert.file(`test/${actionName}.test.js`) 56 | assert.file(`${extFolder}/e2e/${actionName}.e2e.test.js`); 57 | 58 | assert.file(`${basicGeneratorOptions['action-folder']}/utils.js`); 59 | assert.file(`${extFolder}/test/utils.test.js`); 60 | } 61 | 62 | /** 63 | * Checks that a correct action section has been added to the App Builder project configuration file. 64 | * 65 | * @param {string} actionName an action name 66 | */ 67 | function assertManifestContent (actionName) { 68 | const json = yaml.load(fs.readFileSync(basicGeneratorOptions['config-path']).toString()); 69 | expect(json.runtimeManifest.packages).toBeDefined(); 70 | const packages = json.runtimeManifest.packages; 71 | const packageName = Object.keys(packages)[0]; 72 | expect(json.runtimeManifest.packages[packageName].actions[actionName]).toEqual({ 73 | function: `actions/${actionName}/index.js`, 74 | web: 'yes', 75 | runtime: defaultRuntimeKind, 76 | inputs: { 77 | LOG_LEVEL: 'debug', 78 | API_ENDPOINT: '$API_ENDPOINT' 79 | }, 80 | annotations: { 81 | 'final': true, 82 | 'require-adobe-auth': false 83 | } 84 | }); 85 | } 86 | 87 | /** 88 | * Checks that .env has the required environment variables. 89 | */ 90 | function assertEnvContent () { 91 | const theFile = '.env'; 92 | assert.fileContent( 93 | theFile, 94 | '#API_ENDPOINT=' 95 | ); 96 | } 97 | 98 | /** 99 | * Checks that an action file contains correct code snippets. 100 | * 101 | * @param {string} actionName an action name 102 | */ 103 | function assertActionCodeContent (actionName) { 104 | const theFile = `${basicGeneratorOptions['action-folder']}/${actionName}/index.js`; 105 | assert.fileContent( 106 | theFile, 107 | 'const apiEndpoint = `${params.API_ENDPOINT}`' 108 | ); 109 | assert.fileContent( 110 | theFile, 111 | 'const requiredHeaders = [\'Authorization\']' 112 | ); 113 | } 114 | 115 | describe('run', () => { 116 | test('test a generator invocation with custom code generation', async () => { 117 | const options = cloneDeep(basicGeneratorOptions); 118 | await helpers.run(ExtensionActionGenerator) 119 | .withOptions(options) 120 | .inTmpDir(dir => { 121 | // ActionGenerator expects to have the ".env" file created 122 | fs.writeFileSync('.env', '') 123 | }); 124 | 125 | assertGeneratedFiles(actionName); 126 | assertManifestContent(actionName); 127 | assertActionCodeContent(actionName); 128 | assertDependencies( 129 | fs, 130 | { 'node-fetch': expect.any(String) }, 131 | { '@openwhisk/wskdebug': expect.any(String) } 132 | ); 133 | assertEnvContent(); 134 | }); 135 | }) 136 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 Generator = require('yeoman-generator'); 14 | 15 | const ExtensionMainGenerator = require('../src/index'); 16 | const ExtensionActionGenerator = require('../src/generator-add-action'); 17 | const ExtensionWebAssetsGenerator = require('../src/generator-add-web-assets'); 18 | const { utils } = require('@adobe/generator-app-common-lib'); 19 | 20 | const { defaultExtensionManifest, customExtensionManifest } = require('./test-manifests'); 21 | 22 | const composeWith = jest.spyOn(Generator.prototype, 'composeWith').mockImplementation(jest.fn()); 23 | const prompt = jest.spyOn(Generator.prototype, 'prompt') // prompt answers are mocked by "yeoman-test" 24 | const writeKeyAppConfig = jest.spyOn(utils, 'writeKeyAppConfig').mockImplementation(jest.fn()); 25 | const writeKeyYAMLConfig = jest.spyOn(utils, 'writeKeyYAMLConfig').mockImplementation(jest.fn()); 26 | 27 | beforeEach(() => { 28 | composeWith.mockClear(); 29 | prompt.mockClear(); 30 | writeKeyAppConfig.mockClear(); 31 | writeKeyYAMLConfig.mockClear(); 32 | }); 33 | 34 | describe('prototype', () => { 35 | test('exports a yeoman generator', () => { 36 | expect(ExtensionMainGenerator.prototype).toBeInstanceOf(Generator); 37 | }); 38 | }); 39 | 40 | describe('run', () => { 41 | const srcFolder = 'src/aem-assets-browse-1'; 42 | const configName = 'aem/assets/browse/1'; 43 | const extConfig = 'ext.config.yaml'; 44 | 45 | test('test a generator invocation with default code generation', async () => { 46 | const options = { 47 | 'is-test': true, 48 | 'extension-manifest': defaultExtensionManifest 49 | } 50 | await helpers.run(ExtensionMainGenerator) 51 | .withOptions(options); 52 | expect(prompt).not.toHaveBeenCalled(); 53 | expect(composeWith).toHaveBeenCalledTimes(1); 54 | expect(composeWith).toHaveBeenCalledWith( 55 | expect.objectContaining({ 56 | Generator: ExtensionWebAssetsGenerator, 57 | path: 'unknown' 58 | }), 59 | expect.any(Object) 60 | ); 61 | expect(writeKeyAppConfig).toHaveBeenCalledTimes(1); 62 | expect(writeKeyYAMLConfig).toHaveBeenCalledTimes(4); 63 | expect(writeKeyAppConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), `extensions.${configName}`, { $include: `${srcFolder}/${extConfig}` }); 64 | expect(writeKeyYAMLConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), global.n(`${srcFolder}/${extConfig}`), 'operations', { view: [{ impl: 'index.html', type: 'web' }] }); 65 | expect(writeKeyYAMLConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), global.n(`${srcFolder}/${extConfig}`), 'actions', 'actions'); 66 | expect(writeKeyYAMLConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), global.n(`${srcFolder}/${extConfig}`), 'web', 'web-src'); 67 | }); 68 | 69 | test('test a generator invocation with custom code generation', async () => { 70 | const options = { 71 | 'is-test': true, 72 | 'extension-manifest': customExtensionManifest 73 | } 74 | await helpers.run(ExtensionMainGenerator) 75 | .withOptions(options); 76 | expect(prompt).not.toHaveBeenCalled(); 77 | expect(composeWith).toHaveBeenCalledTimes(3); 78 | expect(composeWith).toHaveBeenCalledWith( 79 | expect.objectContaining({ 80 | Generator: ExtensionActionGenerator, 81 | path: 'unknown' 82 | }), 83 | expect.any(Object) 84 | ); 85 | expect(composeWith).toHaveBeenCalledWith( 86 | expect.objectContaining({ 87 | Generator: ExtensionActionGenerator, 88 | path: 'unknown' 89 | }), 90 | expect.any(Object) 91 | ); 92 | expect(composeWith).toHaveBeenCalledWith( 93 | expect.objectContaining({ 94 | Generator: ExtensionWebAssetsGenerator, 95 | path: 'unknown' 96 | }), 97 | expect.any(Object) 98 | ); 99 | expect(writeKeyAppConfig).toHaveBeenCalledTimes(1); 100 | expect(writeKeyYAMLConfig).toHaveBeenCalledTimes(4); 101 | expect(writeKeyAppConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), `extensions.${configName}`, { $include: `${srcFolder}/${extConfig}` }); 102 | expect(writeKeyYAMLConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), global.n(`${srcFolder}/${extConfig}`), 'operations', { view: [{ impl: 'index.html', type: 'web' }] }); 103 | expect(writeKeyYAMLConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), global.n(`${srcFolder}/${extConfig}`), 'actions', 'actions'); 104 | expect(writeKeyYAMLConfig).toHaveBeenCalledWith(expect.any(ExtensionMainGenerator), global.n(`${srcFolder}/${extConfig}`), 'web', 'web-src'); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contribute to a positive environment for our project and community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best, not just for us as individuals but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | * Trolling, insulting or derogatory comments, and personal or political attacks 23 | * Public or private harassment 24 | * Publishing others’ private information, such as a physical or email address, without their explicit permission 25 | * Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Our Responsibilities 28 | 29 | Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any instances of unacceptable behavior. 30 | 31 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for behaviors that they deem inappropriate, threatening, offensive, or harmful. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies when an individual is representing the project or its community both within project spaces and in public spaces. Examples of representing a project or community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by first contacting the project team. Oversight of Adobe projects is handled by the Adobe Open Source Office, which has final say in any violations and enforcement of this Code of Conduct and can be reached at Grp-opensourceoffice@adobe.com. All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | The project team must respect the privacy and security of the reporter of any incident. 42 | 43 | Project maintainers who do not follow or enforce the Code of Conduct may face temporary or permanent repercussions as determined by other members of the project's leadership or the Adobe Open Source Office. 44 | 45 | ## Enforcement Guidelines 46 | 47 | Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem to be in violation of this Code of Conduct: 48 | 49 | **1. Correction** 50 | 51 | Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 52 | 53 | Consequence: A private, written warning from project maintainers describing the violation and why the behavior was unacceptable. A public apology may be requested from the violator before any further involvement in the project by violator. 54 | 55 | **2. Warning** 56 | 57 | Community Impact: A relatively minor violation through a single incident or series of actions. 58 | 59 | Consequence: A written warning from project maintainers that includes stated consequences for continued unacceptable behavior. Violator must refrain from interacting with the people involved for a specified period of time as determined by the project maintainers, including, but not limited to, unsolicited interaction with those enforcing the Code of Conduct through channels such as community spaces and social media. Continued violations may lead to a temporary or permanent ban. 60 | 61 | **3. Temporary Ban** 62 | 63 | Community Impact: A more serious violation of community standards, including sustained unacceptable behavior. 64 | 65 | Consequence: A temporary ban from any interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Failure to comply with the temporary ban may lead to a permanent ban. 66 | 67 | **4. Permanent Ban** 68 | 69 | Community Impact: Demonstrating a consistent pattern of violation of community standards or an egregious violation of community standards, including, but not limited to, sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 70 | 71 | Consequence: A permanent ban from any interaction with the community. 72 | 73 | ## Attribution 74 | 75 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, 76 | available at [https://contributor-covenant.org/version/2/1][version] 77 | 78 | [homepage]: https://contributor-covenant.org 79 | [version]: https://contributor-covenant.org/version/2/1 -------------------------------------------------------------------------------- /src/generator-add-web-assets.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 | const { utils, constants } = require('@adobe/generator-app-common-lib'); 15 | const { commonDependencyVersions } = constants; 16 | 17 | class ExtensionWebAssetsGenerator extends Generator { 18 | constructor(args, opts) { 19 | super(args, opts); 20 | // required 21 | this.option('web-src-folder', {type: String}); 22 | // this.option('skip-prompt', { default: false }) // useless for now 23 | this.option('config-path', {type: String}); 24 | 25 | // props are used by templates 26 | this.props = {}; 27 | this.props['extensionManifest'] = this.options['extension-manifest']; 28 | this.props['projectName'] = utils.readPackageJson(this).name; 29 | } 30 | 31 | writing() { 32 | this.destFolder = this.options['web-src-folder']; 33 | this.sourceRoot(path.join(__dirname, '.')); 34 | 35 | // Copy all the static files 36 | this.fs.copyTpl( 37 | this.templatePath('./templates/web/**/*'), 38 | this.destinationPath(this.destFolder), 39 | this.props 40 | ); 41 | 42 | // Copy hooks folder 43 | this.fs.copyTpl( 44 | this.templatePath('./templates/hooks/*'), 45 | this.destinationPath('./hooks') 46 | ); 47 | 48 | // Generate App.js 49 | this._generateAppRoute(); 50 | 51 | // Generate ExtensionRegistration.js 52 | this._generateExtensionRegistration(); 53 | 54 | // Generate React component files for custom actions that needs to show a modal 55 | this._generateModalFiles('actionBar'); 56 | this._generateModalFiles('headerMenu'); 57 | 58 | // add .babelrc 59 | /// NOTE this is a global file and might conflict 60 | this.fs.writeJSON(this.destinationPath('.babelrc'), { 61 | plugins: ['@babel/plugin-transform-react-jsx'] 62 | }); 63 | // add dependencies 64 | utils.addDependencies(this, { 65 | '@adobe/aio-sdk': commonDependencyVersions['@adobe/aio-sdk'], 66 | '@adobe/exc-app': '^0.2.21', 67 | '@adobe/react-spectrum': '^3.4.0', 68 | '@adobe/uix-guest': '^0.10.5', 69 | '@react-spectrum/list': '^3.0.0-rc.0', 70 | '@spectrum-icons/workflow': '^3.2.0', 71 | 'chalk': '^4', 72 | 'core-js': '^3.6.4', 73 | 'node-fetch': '^2.6.0', 74 | 'node-html-parser': '^5.4.2-0', 75 | 'react': '^16.13.1', 76 | 'react-dom': '^16.13.1', 77 | 'react-error-boundary': '^1.2.5', 78 | 'react-router-dom': '^6.3.0', 79 | 'regenerator-runtime': '^0.13.5' 80 | }); 81 | utils.addDependencies( 82 | this, 83 | { 84 | '@babel/core': '^7.8.7', 85 | '@babel/plugin-transform-react-jsx': '^7.8.3', 86 | '@babel/polyfill': '^7.8.7', 87 | '@babel/preset-env': '^7.8.7', 88 | '@openwhisk/wskdebug': '^1.3.0', 89 | 'jest': '^27.2.4' 90 | }, 91 | true 92 | ); 93 | } 94 | 95 | _generateAppRoute() { 96 | // Generic Project 97 | var relativeTemplatePath = './templates/_shared/stub-app.ejs'; 98 | 99 | this.fs.copyTpl( 100 | this.templatePath(relativeTemplatePath), 101 | this.destinationPath(path.join(this.destFolder, `./src/components/App.js`)), 102 | this.props 103 | ); 104 | } 105 | 106 | _generateExtensionRegistration() { 107 | // Generic Project 108 | var relativeTemplatePath = './templates/_shared/stub-extension-registration.ejs'; 109 | 110 | this.fs.copyTpl( 111 | this.templatePath(relativeTemplatePath), 112 | this.destinationPath(path.join(this.destFolder, `./src/components/ExtensionRegistration.js`)), 113 | this.props 114 | ); 115 | } 116 | 117 | /** 118 | * Generate files for modal dialog with respect to extension area 119 | * @param extensionArea - extension area, e.g. actionBar or headerMenu 120 | * @private 121 | */ 122 | _generateModalFiles(extensionArea) { 123 | // Generic Project 124 | var relativeTemplatePath = './templates/_shared/stub-modal.ejs'; 125 | var customActions = []; 126 | 127 | if (extensionArea === 'actionBar') { 128 | customActions = this.props.extensionManifest.actionBarActions || []; 129 | } else if (extensionArea === 'headerMenu') { 130 | customActions = this.props.extensionManifest.headerMenuButtons || []; 131 | } 132 | 133 | customActions.forEach((action) => { 134 | if (action.needsModal) { 135 | const modalComponentName = action.componentName; 136 | const modalType = action.modalType; 137 | this.fs.copyTpl( 138 | this.templatePath(relativeTemplatePath), 139 | this.destinationPath(path.join(this.destFolder, `./src/components/${modalComponentName}.js`)), { 140 | modalComponentName: modalComponentName, 141 | modalType: modalType, 142 | } 143 | ); 144 | } 145 | }) 146 | } 147 | } 148 | 149 | module.exports = ExtensionWebAssetsGenerator; 150 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 chalk = require('chalk'); 16 | 17 | const ExtensionActionGenerator = require('./generator-add-action'); 18 | const ExtensionWebAssetsGenerator = require('./generator-add-web-assets'); 19 | 20 | const {constants, utils} = require('@adobe/generator-app-common-lib'); 21 | const {runtimeManifestKey} = constants; 22 | const {briefOverviews, promptTopLevelFields, promptMainMenu, promptDocs} = require('./prompts'); 23 | const {readManifest, writeManifest} = require('./utils'); 24 | 25 | const EXTENSION_MANIFEST_FILE = 'extension-manifest.json'; 26 | 27 | /* 28 | 'initializing', 29 | 'prompting', 30 | 'configuring', 31 | 'default', 32 | 'writing', 33 | 'conflicts', 34 | 'install', 35 | 'end' 36 | */ 37 | 38 | class MainGenerator extends Generator { 39 | constructor(args, opts) { 40 | super(args, opts); 41 | 42 | // options are inputs from CLI or yeoman parent generator 43 | this.option('skip-prompt', {default: false}); 44 | this.option('is-test', {default: false}); 45 | } 46 | 47 | async initializing() { 48 | // all paths are relative to root 49 | this.extFolder = 'src/aem-assets-browse-1'; 50 | this.actionFolder = path.join(this.extFolder, 'actions') 51 | this.webSrcFolder = path.join(this.extFolder, 'web-src'); 52 | this.extConfigPath = path.join(this.extFolder, 'ext.config.yaml'); 53 | this.configName = 'aem/assets/browse/1'; 54 | 55 | if (!this.options['is-test']) { 56 | this.extensionManifest = readManifest(EXTENSION_MANIFEST_FILE); 57 | } else { 58 | this.extensionManifest = this.options['extension-manifest']; 59 | } 60 | } 61 | 62 | async prompting() { 63 | if (!this.options['is-test']) { 64 | this.log(briefOverviews['templateInfo']); 65 | await promptTopLevelFields(this.extensionManifest) 66 | .then(() => promptMainMenu(this.extensionManifest)) 67 | .then(() => writeManifest(this.extensionManifest, EXTENSION_MANIFEST_FILE)) 68 | .then(() => { 69 | this.log('\nExtension Manifest for Code Pre-generation'); 70 | this.log("------------------------------------------"); 71 | this.log(JSON.stringify(this.extensionManifest, null, ' ')); 72 | }); 73 | } 74 | } 75 | 76 | async writing() { 77 | // generate the generic action 78 | if (this.extensionManifest.runtimeActions) { 79 | this.extensionManifest.runtimeActions.forEach((action) => { 80 | this.composeWith({ 81 | Generator: ExtensionActionGenerator, 82 | path: 'unknown' 83 | }, 84 | { 85 | // forward needed args 86 | 'action-folder': this.actionFolder, 87 | 'config-path': this.extConfigPath, 88 | 'full-key-to-manifest': runtimeManifestKey, 89 | 'action-name': action.name, 90 | 'extension-manifest': this.extensionManifest 91 | }); 92 | }) 93 | } 94 | 95 | // generate the UI 96 | this.composeWith({ 97 | Generator: ExtensionWebAssetsGenerator, 98 | path: 'unknown' 99 | }, 100 | { 101 | 'skip-prompt': this.options['skip-prompt'], 102 | 'web-src-folder': this.webSrcFolder, 103 | 'config-path': this.extConfigPath, 104 | 'extension-manifest': this.extensionManifest, 105 | }); 106 | 107 | const unixExtConfigPath = upath.toUnix(this.extConfigPath); 108 | // add the extension point config in root 109 | utils.writeKeyAppConfig( 110 | this, 111 | // key 112 | 'extensions.' + this.configName, 113 | // value 114 | { 115 | // posix separator 116 | $include: unixExtConfigPath 117 | } 118 | ); 119 | 120 | // add extension point operation 121 | utils.writeKeyYAMLConfig( 122 | this, 123 | this.extConfigPath, 124 | // key 125 | 'operations', { 126 | view: [ 127 | {type: 'web', impl: 'index.html'} 128 | ] 129 | } 130 | ); 131 | 132 | // add actions path, relative to config file 133 | utils.writeKeyYAMLConfig(this, this.extConfigPath, 'actions', path.relative(this.extFolder, this.actionFolder)); 134 | 135 | // add web-src path, relative to config file 136 | utils.writeKeyYAMLConfig(this, this.extConfigPath, 'web', path.relative(this.extFolder, this.webSrcFolder)); 137 | 138 | // add hooks path 139 | utils.writeKeyYAMLConfig(this, this.extConfigPath, 140 | 'hooks', { 141 | 'post-app-deploy': './hooks/post-deploy.js' 142 | } 143 | ); 144 | } 145 | 146 | async end() { 147 | this.log(chalk.bold('\nSample code files have been generated.\n')); 148 | this.log(chalk.bold('Next Steps:')); 149 | this.log(chalk.bold('-----------')); 150 | this.log(chalk.bold('1) Populate your local environment variables in the ".env" file.')); 151 | this.log(chalk.bold('2) You can use `aio app run` or `aio app deploy` to see the sample code files in action.')); 152 | this.log('\n'); 153 | } 154 | } 155 | 156 | module.exports = MainGenerator; 157 | -------------------------------------------------------------------------------- /test/generator-add-web-assets.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 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 | /* eslint-disable jest/expect-expect */ // => use assert 13 | 14 | const helpers = require('yeoman-test'); 15 | const assert = require('yeoman-assert'); 16 | const fs = require('fs'); 17 | const path = require('path') 18 | const cloneDeep = require('lodash.clonedeep'); 19 | 20 | const ExtensionWebAssetsGenerator = require('../src/generator-add-web-assets'); 21 | const Generator = require('yeoman-generator'); 22 | 23 | const { defaultExtensionManifest, customExtensionManifest } = require('./test-manifests'); 24 | 25 | const extFolder = 'src/aem-assets-browse-1'; 26 | const extConfigPath = path.join(extFolder, 'ext.config.yaml'); 27 | const webSrcFolder = path.join(extFolder, 'web-src'); 28 | 29 | const basicGeneratorOptions = { 30 | 'web-src-folder': webSrcFolder, 31 | 'config-path': extConfigPath 32 | }; 33 | 34 | describe('prototype', () => { 35 | test('exports a yeoman generator', () => { 36 | expect(ExtensionWebAssetsGenerator.prototype).toBeInstanceOf(Generator); 37 | }); 38 | }); 39 | 40 | /** 41 | * Checks that .env has the required environment variables. 42 | */ 43 | function assertEnvContent(prevContent) { 44 | assert.fileContent('.env', prevContent); 45 | } 46 | 47 | /** 48 | * Checks that all the files are generated. 49 | * 50 | * @param {string} extensionManifest an extension manifest 51 | */ 52 | function assertFiles(extensionManifest) { 53 | // Assert generated web assets files 54 | assert.file(`${webSrcFolder}/index.html`); 55 | assert.file(`${webSrcFolder}/src/index.css`); 56 | assert.file(`${webSrcFolder}/src/index.js`); 57 | assert.file(`${webSrcFolder}/src/components/Constants.js`); 58 | assert.file(`${webSrcFolder}/src/components/App.js`); 59 | assert.file(`${webSrcFolder}/src/components/ExtensionRegistration.js`); 60 | } 61 | 62 | /** 63 | * Checks that files contains the correct code snippets. 64 | * 65 | * @param {string} extensionManifest an extension manifest 66 | */ 67 | function assertCodeContent(extensionManifest) { 68 | assert.fileContent( 69 | `${webSrcFolder}/src/components/Constants.js`, 70 | `extensionId = '${extensionManifest.id}'` 71 | ); 72 | 73 | assert.fileContent( 74 | `${webSrcFolder}/index.html`, 75 | '