├── .azure-devops ├── build.yml ├── cleanup.yml ├── devcerts.yml ├── edgewebview.yml ├── full-pipeline.yml ├── install.yml ├── lint.yml └── test.yml ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── issue-with-building-running-custom-functions.md │ └── issue-with-the-excel-custom-functions-sample.md ├── policies │ └── resourceManagement.yml └── pull_request_template.md ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── color.png ├── icon-128.png ├── icon-16.png ├── icon-32.png ├── icon-64.png ├── icon-80.png ├── logo-filled.png └── outline.png ├── babel.config.json ├── convertToSingleHost.js ├── manifest.json ├── manifest.xml ├── package-lock.json ├── package.json ├── src ├── commands │ ├── commands.html │ └── commands.js ├── functions │ ├── functions.html │ └── functions.js └── taskpane │ ├── taskpane.css │ ├── taskpane.html │ └── taskpane.js ├── test ├── end-to-end │ ├── src │ │ ├── debugger-websocket.ts │ │ ├── test-commands.html │ │ ├── test-data.json │ │ ├── test-helpers.ts │ │ ├── test-taskpane.html │ │ ├── test-taskpane.ts │ │ └── typings.d.ts │ ├── test-manifest-debugging.xml │ ├── test-manifest.xml │ ├── tests.ts │ └── webpack.config.js ├── tsconfig.json └── unit │ └── excel.test.ts ├── tsconfig.json └── webpack.config.js /.azure-devops/build.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: Npm@1 3 | displayName: 'Build' 4 | inputs: 5 | command: custom 6 | customCommand: 'run build' -------------------------------------------------------------------------------- /.azure-devops/cleanup.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: mspremier.PostBuildCleanup.PostBuildCleanup-task.PostBuildCleanup@3 3 | displayName: "Cleanup" -------------------------------------------------------------------------------- /.azure-devops/devcerts.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | echo Install Office-AddinDev-Certs at machine level 4 | call npx office-addin-dev-certs install --machine 5 | displayName: 'Install add-in dev cert' -------------------------------------------------------------------------------- /.azure-devops/edgewebview.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | echo Enable EdgeWebView Loopback 4 | call npx office-addin-dev-settings appcontainer EdgeWebView --loopback --yes 5 | echo Set Edge WebView Registry Settings 6 | set PATH1="HKEY_CURRENT_USER\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppContainer\Mappings\S-1-15-2-1310292540-1029022339-4008023048-2190398717-53961996-4257829345-603366646" 7 | reg add %PATH1% /f /v DisplayName /t REG_SZ /d "@{Microsoft.Win32WebViewHost_10.0.19041.423_neutral_neutral_cw5n1h2txyewy?ms-resource://Windows.Win32WebViewHost/resources/DisplayName}" 8 | reg add %PATH1% /f /v Moniker /t REG_SZ /d "microsoft.win32webviewhost_cw5n1h2txyewy" 9 | set PATH2="HKEY_CURRENT_USER\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppContainer\Mappings\S-1-15-2-1310292540-1029022339-4008023048-2190398717-53961996-4257829345-603366646\Children\S-1-15-2-1310292540-1029022339-4008023048-2190398717-53961996-4257829345-603366646-3829197285-1050560373-949424154-522343454" 10 | reg add %PATH2% /f /v DisplayName /t REG_SZ /d "microsoft.win32webviewhost_cw5n1h2txyewy/123" 11 | reg add %PATH2% /f /v Moniker /t REG_SZ /d "123" 12 | reg add %PATH2% /f /v ParentMoniker /t REG_SZ /d "microsoft.win32webviewhost_cw5n1h2txyewy" 13 | displayName: 'Enable Edge WebView' -------------------------------------------------------------------------------- /.azure-devops/full-pipeline.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | repositories: 3 | - repository: OfficePipelineTemplates 4 | type: git 5 | name: 1ESPipelineTemplates/OfficePipelineTemplates 6 | ref: refs/tags/release 7 | extends: 8 | template: /v1/Office.Official.PipelineTemplate.yml@OfficePipelineTemplates 9 | parameters: 10 | pool: 11 | name: OE-OfficeClientApps 12 | sdl: 13 | eslint: 14 | configuration: required 15 | parser: '@typescript-eslint/parser' 16 | parserOptions: sourceType:module 17 | stages: 18 | - stage: stage 19 | jobs: 20 | - job: Windows_10_Latest 21 | steps: 22 | - template: /.azure-devops/install.yml@self 23 | - template: /.azure-devops/lint.yml@self 24 | - template: /.azure-devops/build.yml@self 25 | - template: /.azure-devops/devcerts.yml@self 26 | - template: /.azure-devops/edgewebview.yml@self 27 | - template: /.azure-devops/test.yml@self 28 | parameters: 29 | webView: "edge-chromium" 30 | - job: WebView_EdgeLegacy 31 | steps: 32 | - template: /.azure-devops/install.yml@self 33 | - template: /.azure-devops/lint.yml@self 34 | - template: /.azure-devops/build.yml@self 35 | - template: /.azure-devops/devcerts.yml@self 36 | - template: /.azure-devops/edgewebview.yml@self 37 | - template: /.azure-devops/test.yml@self 38 | parameters: 39 | webView: "edge-legacy" 40 | # Need to determin why the IE test doesn't pass in the lab 41 | # - job: WebView_IE 42 | # steps: 43 | # - template: /.azure-devops/install.yml@self 44 | # - template: /.azure-devops/lint.yml@self 45 | # - template: /.azure-devops/build.yml@self 46 | # - template: /.azure-devops/devcerts.yml@self 47 | # - template: /.azure-devops/edgewebview.yml@self 48 | # - template: /.azure-devops/test.yml@self 49 | # parameters: 50 | # webView: "ie" 51 | -------------------------------------------------------------------------------- /.azure-devops/install.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: Npm@1 3 | displayName: 'Install' -------------------------------------------------------------------------------- /.azure-devops/lint.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: Npm@1 3 | displayName: 'Lint' 4 | inputs: 5 | command: custom 6 | customCommand: 'run lint' -------------------------------------------------------------------------------- /.azure-devops/test.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: webView 3 | type: string 4 | default: "edge-chromium" 5 | steps: 6 | - task: CmdLine@2 7 | inputs: 8 | script: | 9 | echo Setting WebView Type: ${{ parameters.webView }} 10 | call npx office-addin-dev-settings webview manifest.xml ${{ parameters.webView }} 11 | call npx office-addin-dev-settings webview test/end-to-end/test-manifest.xml ${{ parameters.webView }} 12 | echo Running Tests 13 | npm run test 14 | echo Done running tests 15 | displayName: 'Run Tests' -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "office-addins" 4 | ], 5 | "extends": [ 6 | "plugin:office-addins/recommended" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @OfficeDev/office-platform-devx -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Prerequisites 11 | 12 | Please answer the following questions before submitting an issue. 13 | **YOU MAY DELETE THE PREREQUISITES SECTION.** 14 | - [ ] I am running the latest version of Node and the tools 15 | - [ ] I checked the documentation and found no answer 16 | - [ ] I checked to make sure that this issue has not already been filed 17 | 18 | 19 | # Expected behavior 20 | 21 | Please describe the behavior you were expecting 22 | 23 | 24 | # Current behavior 25 | 26 | Please provide information about the failure. What is the current behavior? If it is not a bug, please submit your idea to the [Microsoft Tech Community Ideas forum](https://techcommunity.microsoft.com/t5/microsoft-365-developer-platform/idb-p/Microsoft365DeveloperPlatform), so that it gets added to our feature roadmap. 27 | 28 | 29 | ## Steps to Reproduce 30 | 31 | Please provide detailed steps for reproducing the issue. 32 | 33 | 1. step 1 34 | 2. step 2 35 | 3. you get it... 36 | 37 | 38 | ## Context 39 | 40 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. 41 | 42 | * Operating System: 43 | * Node version: 44 | * Office version: 45 | * Tool version: 46 | 47 | ## Failure Logs 48 | 49 | Please include any relevant log snippets, screenshots or code samples here. 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-with-building-running-custom-functions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue with building/running custom functions 3 | about: 'Please log these issues in the office-js repo: https://github.com/OfficeDev/office-js/issues' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | *** NOTE: If you have an issue running your custom function, please log an issue in the official office-js repo here: [https://github.com/OfficeDev/office-js/issues](https://github.com/OfficeDev/office-js/issues) *** 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-with-the-excel-custom-functions-sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue with the Excel Custom Functions sample 3 | about: This repo is for logging issues with the sample code 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | *** This repo is for issues related to the current Excel Custom Functions sample. *** 11 | 12 | If you have an issue _running_ your custom function, please log an issue in the official office-js repo here: [https://github.com/OfficeDev/office-js/issues](https://github.com/OfficeDev/office-js/issues) *** 13 | -------------------------------------------------------------------------------- /.github/policies/resourceManagement.yml: -------------------------------------------------------------------------------- 1 | id: 2 | name: GitOps.PullRequestIssueManagement 3 | description: GitOps.PullRequestIssueManagement primitive 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | scheduledSearches: 11 | - description: 12 | frequencies: 13 | - hourly: 14 | hour: 3 15 | filters: 16 | - isIssue 17 | - isOpen 18 | - hasLabel: 19 | label: 'Needs: author feedback' 20 | - noActivitySince: 21 | days: 4 22 | - isNotLabeledWith: 23 | label: 'Status: no recent activity' 24 | actions: 25 | - addLabel: 26 | label: 'Status: no recent activity' 27 | - addReply: 28 | reply: This issue has been automatically marked as stale because it is marked as needing author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**. Thank you for your interest in Office Add-ins! 29 | - description: 30 | frequencies: 31 | - hourly: 32 | hour: 3 33 | filters: 34 | - isIssue 35 | - isOpen 36 | - hasLabel: 37 | label: 'Needs: author feedback' 38 | - hasLabel: 39 | label: 'Status: no recent activity' 40 | - noActivitySince: 41 | days: 3 42 | actions: 43 | - addReply: 44 | reply: This issue has been closed due to inactivity. Please comment if you still need assistance and we'll re-open the issue. 45 | - closeIssue 46 | eventResponderTasks: 47 | - if: 48 | - payloadType: Issues 49 | - isAction: 50 | action: Opened 51 | - not: isAssignedToSomeone 52 | then: 53 | - addLabel: 54 | label: 'Needs: triage :mag:' 55 | description: 56 | - if: 57 | - payloadType: Issues 58 | - not: 59 | isAction: 60 | action: Closed 61 | - hasLabel: 62 | label: 'Status: no recent activity' 63 | then: 64 | - removeLabel: 65 | label: 'Status: no recent activity' 66 | description: 67 | - if: 68 | - payloadType: Issue_Comment 69 | - hasLabel: 70 | label: 'Status: no recent activity' 71 | then: 72 | - removeLabel: 73 | label: 'Status: no recent activity' 74 | description: 75 | - if: 76 | - payloadType: Issue_Comment 77 | - isAction: 78 | action: Created 79 | - isActivitySender: 80 | issueAuthor: True 81 | - hasLabel: 82 | label: 'Needs: author feedback' 83 | then: 84 | - addLabel: 85 | label: 'Needs: attention :wave:' 86 | - removeLabel: 87 | label: 'Needs: author feedback' 88 | description: 89 | onFailure: 90 | onSuccess: 91 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thank you for your pull request! Please provide the following information. 2 | 3 | --- 4 | 5 | **Change Description**: 6 | 7 | Describe what the PR is about. 8 | 9 | 1. **Do these changes impact any *npm scripts* commands (in package.json)?** (e.g., running 'npm run start') 10 | If Yes, briefly describe what is impacted. 11 | 12 | 13 | 2. **Do these changes impact *VS Code debugging* options (launch.json)?** 14 | If Yes, briefly describe what is impacted. 15 | 16 | 17 | 3. **Do these changes impact *template output*?** (e.g., add/remove file, update file location, update file contents) 18 | If Yes, briefly describe what is impacted. 19 | 20 | 21 | 4. **Do these changes impact *documentation*?** (e.g., a tutorial on https://docs.microsoft.com/en-us/office/dev/add-ins/overview/office-add-ins) 22 | If Yes, briefly describe what is impacted. 23 | 24 | 25 | If you answered yes to any of these please do the following: 26 | > Include 'Rick-Kirkham' in the review 27 | > Make sure the README file is correct 28 | 29 | **Validation/testing performed**: 30 | 31 | Describe manual testing done. 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | dist/ 3 | 4 | # Node modules folder 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "ms-edgedevtools.vscode-edge-devtools", 8 | "dbaeumer.vscode-eslint", 9 | "esbenp.prettier-vscode", 10 | "msoffice.microsoft-office-add-in-debugger" 11 | ], 12 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 13 | "unwantedRecommendations": [] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug UI Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 9 | "args": [ 10 | "-u", 11 | "bdd", 12 | "--timeout", 13 | "999999", 14 | "--colors", 15 | "${workspaceFolder}/test/end-to-end", 16 | "-r", 17 | "ts-node/register", 18 | "${workspaceFolder}/test/end-to-end/*.ts" 19 | ], 20 | "internalConsoleOptions": "openOnSessionStart", 21 | "runtimeArgs": ["--preserve-symlinks"] 22 | }, 23 | { 24 | "name": "Debug Unit Tests", 25 | "type": "node", 26 | "request": "launch", 27 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 28 | "args": [ 29 | "-u", 30 | "bdd", 31 | "--timeout", 32 | "999999", 33 | "--colors", 34 | "${workspaceFolder}/test/unit", 35 | "-r", 36 | "ts-node/register", 37 | "${workspaceFolder}/test/unit/*.test.ts" 38 | ], 39 | "internalConsoleOptions": "openOnSessionStart", 40 | "runtimeArgs": ["--preserve-symlinks"] 41 | }, 42 | { 43 | "name": "Excel Desktop (Edge Chromium)", 44 | "type": "msedge", 45 | "request": "attach", 46 | "port": 9229, 47 | "timeout": 600000, 48 | "webRoot": "${workspaceRoot}", 49 | "preLaunchTask": "Debug: Excel Desktop", 50 | "postDebugTask": "Stop Debug" 51 | }, 52 | { 53 | "name": "Excel Desktop (Edge Legacy)", 54 | "type": "office-addin", 55 | "request": "attach", 56 | "url": "https://localhost:3000/taskpane.html?_host_Info=Excel$Win32$16.01$en-US$$$$0", 57 | "port": 9222, 58 | "timeout": 600000, 59 | "webRoot": "${workspaceRoot}", 60 | "preLaunchTask": "Debug: Excel Desktop", 61 | "postDebugTask": "Stop Debug" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build (Development)", 6 | "type": "npm", 7 | "script": "build:dev", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "presentation": { 13 | "clear": true, 14 | "panel": "shared", 15 | "showReuseMessage": false 16 | } 17 | }, 18 | { 19 | "label": "Build (Production)", 20 | "type": "npm", 21 | "script": "build", 22 | "group": "build", 23 | "presentation": { 24 | "clear": true, 25 | "panel": "shared", 26 | "showReuseMessage": false 27 | } 28 | }, 29 | { 30 | "label": "Debug: Excel Desktop", 31 | "type": "shell", 32 | "command": "npm", 33 | "args": [ 34 | "run", 35 | "start", 36 | "--", 37 | "desktop", 38 | "--app", 39 | "excel" 40 | ], 41 | "presentation": { 42 | "clear": true, 43 | "panel": "dedicated" 44 | }, 45 | "problemMatcher": [] 46 | }, 47 | { 48 | "label": "Dev Server", 49 | "type": "npm", 50 | "script": "dev-server", 51 | "presentation": { 52 | "clear": true, 53 | "panel": "dedicated" 54 | }, 55 | "problemMatcher": [] 56 | }, 57 | { 58 | "label": "Install", 59 | "type": "npm", 60 | "script": "install", 61 | "presentation": { 62 | "clear": true, 63 | "panel": "shared", 64 | "showReuseMessage": false 65 | }, 66 | "problemMatcher": [] 67 | }, 68 | { 69 | "label": "Lint: Check for problems", 70 | "type": "npm", 71 | "script": "lint", 72 | "problemMatcher": [ 73 | "$eslint-stylish" 74 | ] 75 | }, 76 | { 77 | "label": "Lint: Fix all auto-fixable problems", 78 | "type": "npm", 79 | "script": "lint:fix", 80 | "problemMatcher": [ 81 | "$eslint-stylish" 82 | ] 83 | }, 84 | { 85 | "label": "Stop Debug", 86 | "type": "npm", 87 | "script": "stop", 88 | "presentation": { 89 | "clear": true, 90 | "panel": "shared", 91 | "showReuseMessage": false 92 | }, 93 | "problemMatcher": [] 94 | }, 95 | { 96 | "label": "Watch", 97 | "type": "npm", 98 | "script": "watch", 99 | "presentation": { 100 | "clear": true, 101 | "panel": "dedicated" 102 | }, 103 | "problemMatcher": [] 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to this code sample 2 | 3 | Thank you for your interest in this sample! Your contributions and improvements will help the developer community. 4 | 5 | ## Ways to contribute 6 | 7 | There are several ways you can contribute to this sample: providing better code comments, fixing open issues, and adding new features. 8 | 9 | ### Provide better code comments 10 | 11 | Code comments make code samples even better by helping developers learn to use the code correctly in their own applications. If you spot a class, method, or section of code that you think could use better descriptions, then create a pull request with your code comments. 12 | 13 | 14 | In general, we want our code comments to follow these guidelines: 15 | 16 | - Any code that has associated documentation displayed in an IDE (such as IntelliSense, or JavaDocs) has code comments. 17 | - Classes, methods, parameters, and return values have clear descriptions. 18 | - Exceptions and errors are documented. 19 | - Remarks exist for anything special or notable about the code. 20 | - Sections of code that have complex algorithms have appropriate comments describing what they do. 21 | - Code added from Stack Overflow, or any other source, is clearly attributed. 22 | 23 | ### Fix open issues 24 | 25 | Sometimes we get a lot of issues, and it can be hard to keep up. If you have a solution to an open issue that hasn't been addressed, fix the issue and then submit a pull request. 26 | 27 | ### Add a new feature 28 | 29 | New features are great! Be sure to check with the repository admin first to be sure the feature fits the intent of the sample. Start by opening an issue in the repository that proposes and describes the feature. The repository admin will respond and may ask for more information. If the admin agrees to the new feature, create the feature and submit a pull request. 30 | 31 | ## Contribution guidelines 32 | 33 | We have some guidelines to help maintain a healthy repo and code for everyone. 34 | 35 | ### The Contribution License Agreement 36 | 37 | For most contributions, we ask you to sign a Contribution License Agreement (CLA). This will happen when you submit a pull request. Microsoft will send a link to the CLA to sign via email. Once you sign the CLA, your pull request can proceed. Read the CLA carefully, because you may need to have your employer sign it. 38 | 39 | ### Code contribution checklist 40 | 41 | Be sure to satisfy all of the requirements in the following list before submitting a pull request: 42 | 43 | - Follow the code style that is appropriate for the platform and language in this repo. For example, Android code follows the style conventions found in the [Code Style for Contributors guide](https://source.android.com/source/code-style.html). 44 | - Test your code. 45 | - Test the UI thoroughly to be sure your change hasn't broken anything. 46 | - Keep the size of your code change reasonable. If the repository owner cannot review your code change in 4 hours or less, your pull request may not be reviewed and approved quickly. 47 | - Avoid unnecessary changes. The reviewer will check differences between your code and the original code. Whitespace changes are called out along with your code. Be sure your changes will help improve the content. 48 | 49 | ### Submit a pull request to the master branch 50 | 51 | When you're finished with your work and are ready to have it merged into the master repository, follow these steps. Note: pull requests are typically reviewed within 10 business days. If your pull request is accepted you will be credited for your submission. 52 | 53 | 1. Submit your pull request against the master branch. 54 | 2. Sign the CLA, if you haven't already done so. 55 | 3. One of the repo admins will process your pull request, including performing a code review. If there are questions, discussions, or change requests in the pull request, be sure to respond. 56 | 4. When the repo admins are satisfied, they will accept and merge the pull request. 57 | 58 | Congratulations, you have successfully contributed to the sample! 59 | 60 | ## FAQ 61 | 62 | ### Where do I get a Contributor's License Agreement? 63 | 64 | If your pull request requires one, you'll automatically be sent a notice that you need to sign the Contributor's License Agreement (CLA). 65 | 66 | As a community member, you must sign the CLA before you can contribute large submissions to this project. You only need complete and submit the CLA document once. Carefully review the document. You may be required to have your employer sign the document. 67 | 68 | ### What happens with my contributions? 69 | 70 | When you submit your changes via a pull request, our team will be notified and will review your pull request. You'll receive notifications about your pull request from GitHub; you may also be notified by someone from our team if we need more information. We reserve the right to edit your submission for legal, style, clarity, or other issues. 71 | 72 | ### Who approves pull requests? 73 | 74 | The admin of the repository approves pull requests. 75 | 76 | ### How soon will I get a response about my change request or issue? 77 | 78 | We typically review pull requests and respond to issues within 10 business days. 79 | 80 | ## More resources 81 | 82 | - To learn more about Markdown, see [Daring Fireball](http://daringfireball.net/). 83 | - To learn more about using Git and GitHub, check out the [GitHub Help section](http://help.github.com/). 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Excel-Custom-Functions-JS 2 | 3 | MIT License 4 | 5 | Copyright (c) Microsoft Corporation. All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom functions in Excel 2 | 3 | Custom functions enable you to add new functions to Excel by defining those functions in JavaScript as part of an add-in. Users within Excel can access custom functions just as they would any native function in Excel, such as `SUM()`. 4 | 5 | This repository contains the source code used by the [Yo Office generator](https://github.com/OfficeDev/generator-office) when you create a new custom functions project. You can also use this repository as a sample to base your own custom functions project from if you choose not to use the generator. For more detailed information about custom functions in Excel, see the [Custom functions overview](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-overview) article in the Office Add-ins documentation or see the [additional resources](#additional-resources) section of this repository. 6 | 7 | ## Debugging custom functions 8 | 9 | This template supports debugging custom functions from [Visual Studio Code](https://code.visualstudio.com/). For more information see [Custom functions debugging](https://aka.ms/custom-functions-debug). For general information on debugging task panes and other Office Add-in parts, see [Test and debug Office Add-ins](https://learn.microsoft.com/office/dev/add-ins/testing/test-debug-office-add-ins). 10 | 11 | ## Questions and comments 12 | 13 | We'd love to get your feedback about this sample. You can send your feedback to us in the *Issues* section of this repository. 14 | 15 | Questions about Office Add-ins development in general should be posted to [Microsoft Q&A](https://learn.microsoft.com/answers/questions/185087/questions-about-office-add-ins.html). If your question is about the Office JavaScript APIs, make sure it's tagged with [office-js-dev]. 16 | 17 | ## Join the Microsoft 365 Developer Program 18 | 19 | Join the [Microsoft 365 Developer Program](https://aka.ms/m365devprogram) to get resources and information to help you build solutions for the Microsoft 365 platform, including recommendations tailored to your areas of interest. 20 | 21 | You might also qualify for a free developer subscription that's renewable for 90 days and comes configured with sample data; for details, see the [FAQ](https://learn.microsoft.com/office/developer-program/microsoft-365-developer-program-faq#who-qualifies-for-a-microsoft-365-e5-developer-subscription-). 22 | 23 | ## Additional resources 24 | 25 | - [Custom functions overview](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-overview) 26 | - [Custom functions best practices](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-best-practices) 27 | - [Custom functions runtime](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-runtime) 28 | - [Office Add-ins documentation](https://learn.microsoft.com/office/dev/add-ins/overview/office-add-ins) 29 | - [Office Add-ins samples](https://github.com/OfficeDev/Office-Add-in-samples) 30 | 31 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 32 | 33 | ## Copyright 34 | 35 | Copyright (c) 2019 Microsoft Corporation. All rights reserved. 36 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/color.png -------------------------------------------------------------------------------- /assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/icon-128.png -------------------------------------------------------------------------------- /assets/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/icon-16.png -------------------------------------------------------------------------------- /assets/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/icon-32.png -------------------------------------------------------------------------------- /assets/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/icon-64.png -------------------------------------------------------------------------------- /assets/icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/icon-80.png -------------------------------------------------------------------------------- /assets/logo-filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/logo-filled.png -------------------------------------------------------------------------------- /assets/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-Custom-Functions-JS/11a470da5679ba34d996165828fa2d1264f40936/assets/outline.png -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "esmodules": false 8 | } 9 | } 10 | ], 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /convertToSingleHost.js: -------------------------------------------------------------------------------- 1 | // NOTE: This script is generally in sync with other template repos (with minor differences) even though this template doesn't support different hosts. 2 | // It's easier to maintain the script in one place and copy it over to other repos than to maintain multiple versions of the script. 3 | 4 | /* global require, process, console */ 5 | 6 | const fs = require("fs"); 7 | const path = require("path"); 8 | const util = require("util"); 9 | const childProcess = require("child_process"); 10 | const hosts = ["excel"]; 11 | 12 | if (process.argv.length <= 2) { 13 | const hostList = hosts.map((host) => `'${host}'`).join(", "); 14 | console.log("SYNTAX: convertToSingleHost.js "); 15 | console.log(); 16 | console.log(` host (required): Specifies which Office app will host the add-in: ${hostList}`); 17 | console.log(` manifestType: Specify the type of manifest to use: 'xml' or 'json'. Defaults to 'xml'`); 18 | console.log(` projectName: The name of the project (use quotes when there are spaces in the name). Defaults to 'My Office Add-in'`); 19 | console.log(` appId: The id of the project or 'random' to generate one. Defaults to 'random'`); 20 | console.log(); 21 | process.exit(1); 22 | } 23 | 24 | const host = process.argv[2]; 25 | const manifestType = process.argv[3]; 26 | const projectName = process.argv[4]; 27 | let appId = process.argv[5]; 28 | const testPackages = [ 29 | "@types/mocha", 30 | "@types/node", 31 | "mocha", 32 | "office-addin-mock", 33 | "office-addin-test-helpers", 34 | "office-addin-test-server", 35 | "ts-node", 36 | ]; 37 | const readFileAsync = util.promisify(fs.readFile); 38 | const unlinkFileAsync = util.promisify(fs.unlink); 39 | const writeFileAsync = util.promisify(fs.writeFile); 40 | 41 | async function modifyProjectForSingleHost(host) { 42 | if (!host) { 43 | throw new Error("The host was not provided."); 44 | } 45 | if (!hosts.includes(host)) { 46 | throw new Error(`'${host}' is not a supported host.`); 47 | } 48 | await convertProjectToSingleHost(host); 49 | await updatePackageJsonForSingleHost(host); 50 | await updateLaunchJsonFile(host); 51 | } 52 | 53 | async function convertProjectToSingleHost(host) { 54 | // NOTE: This template only supports Excel, so we don't need to deal with host specific files. 55 | 56 | // Copy host-specific manifest over manifest.xml 57 | // const manifestContent = await readFileAsync(`./manifest.${host}.xml`, "utf8"); 58 | // await writeFileAsync(`./manifest.xml`, manifestContent); 59 | 60 | // Copy over host-specific taskpane code to taskpane.ts 61 | // const srcContent = await readFileAsync(`./src/taskpane/${host}.ts`, "utf8"); 62 | // await writeFileAsync(`./src/taskpane/taskpane.ts`, srcContent); 63 | 64 | // Delete all host-specific files 65 | // hosts.forEach(async function (host) { 66 | // await unlinkFileAsync(`./manifest.${host}.xml`); 67 | // await unlinkFileAsync(`./src/taskpane/${host}.ts`); 68 | // }); 69 | 70 | // Delete test folder 71 | deleteFolder(path.resolve(`./test`)); 72 | 73 | // Delete the .github folder 74 | deleteFolder(path.resolve(`./.github`)); 75 | 76 | // Delete CI/CD pipeline files 77 | deleteFolder(path.resolve(`./.azure-devops`)); 78 | 79 | // Delete repo support files 80 | await deleteSupportFiles(); 81 | } 82 | 83 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 84 | async function updatePackageJsonForSingleHost(host) { 85 | // Update package.json to reflect selected host 86 | const packageJson = `./package.json`; 87 | const data = await readFileAsync(packageJson, "utf8"); 88 | let content = JSON.parse(data); 89 | 90 | // Update 'config' section in package.json to use selected host 91 | //content.config["app_to_debug"] = host; 92 | 93 | // Remove 'engines' section 94 | delete content.engines; 95 | 96 | // Remove scripts that are unrelated to the selected host 97 | Object.keys(content.scripts).forEach(function (key) { 98 | if (key === "convert-to-single-host") { 99 | delete content.scripts[key]; 100 | } 101 | }); 102 | 103 | // Remove test-related scripts 104 | Object.keys(content.scripts).forEach(function (key) { 105 | if (key.includes("test")) { 106 | delete content.scripts[key]; 107 | } 108 | }); 109 | 110 | // Remove test-related packages 111 | Object.keys(content.devDependencies).forEach(function (key) { 112 | if (testPackages.includes(key)) { 113 | delete content.devDependencies[key]; 114 | } 115 | }); 116 | 117 | // Write updated JSON to file 118 | await writeFileAsync(packageJson, JSON.stringify(content, null, 2)); 119 | } 120 | 121 | async function updateLaunchJsonFile(host) { 122 | // Remove 'Debug Tests' configuration from launch.json 123 | const launchJson = `.vscode/launch.json`; 124 | const launchJsonContent = await readFileAsync(launchJson, "utf8"); 125 | let content = JSON.parse(launchJsonContent); 126 | content.configurations = content.configurations.filter(function (config) { 127 | return config.name.startsWith(getHostName(host)); 128 | }); 129 | await writeFileAsync(launchJson, JSON.stringify(content, null, 2)); 130 | } 131 | 132 | function getHostName(host) { 133 | switch (host) { 134 | case "excel": 135 | return "Excel"; 136 | case "onenote": 137 | return "OneNote"; 138 | case "outlook": 139 | return "Outlook"; 140 | case "powerpoint": 141 | return "PowerPoint"; 142 | case "project": 143 | return "Project"; 144 | case "word": 145 | return "Word"; 146 | default: 147 | throw new Error(`'${host}' is not a supported host.`); 148 | } 149 | } 150 | 151 | function deleteFolder(folder) { 152 | try { 153 | if (fs.existsSync(folder)) { 154 | fs.readdirSync(folder).forEach(function (file) { 155 | const curPath = `${folder}/${file}`; 156 | 157 | if (fs.lstatSync(curPath).isDirectory()) { 158 | deleteFolder(curPath); 159 | } else { 160 | fs.unlinkSync(curPath); 161 | } 162 | }); 163 | fs.rmdirSync(folder); 164 | } 165 | } catch (err) { 166 | throw new Error(`Unable to delete folder "${folder}".\n${err}`); 167 | } 168 | } 169 | 170 | async function deleteSupportFiles() { 171 | await unlinkFileAsync("CONTRIBUTING.md"); 172 | await unlinkFileAsync("LICENSE"); 173 | await unlinkFileAsync("README.md"); 174 | await unlinkFileAsync("SECURITY.md"); 175 | await unlinkFileAsync("./convertToSingleHost.js"); 176 | await unlinkFileAsync(".npmrc"); 177 | await unlinkFileAsync("package-lock.json"); 178 | } 179 | 180 | async function deleteJSONManifestRelatedFiles() { 181 | await unlinkFileAsync("manifest.json"); 182 | await unlinkFileAsync("assets/color.png"); 183 | await unlinkFileAsync("assets/outline.png"); 184 | } 185 | 186 | async function deleteXMLManifestRelatedFiles() { 187 | await unlinkFileAsync("manifest.xml"); 188 | } 189 | 190 | async function updatePackageJsonForXMLManifest() { 191 | const packageJson = `./package.json`; 192 | const data = await readFileAsync(packageJson, "utf8"); 193 | let content = JSON.parse(data); 194 | 195 | // Write updated JSON to file 196 | await writeFileAsync(packageJson, JSON.stringify(content, null, 2)); 197 | } 198 | 199 | async function updatePackageJsonForJSONManifest() { 200 | const packageJson = `./package.json`; 201 | const data = await readFileAsync(packageJson, "utf8"); 202 | let content = JSON.parse(data); 203 | 204 | // Change manifest file name extension 205 | content.scripts.start = "office-addin-debugging start manifest.json"; 206 | content.scripts.stop = "office-addin-debugging stop manifest.json"; 207 | content.scripts.validate = "office-addin-manifest validate manifest.json"; 208 | 209 | // Write updated JSON to file 210 | await writeFileAsync(packageJson, JSON.stringify(content, null, 2)); 211 | } 212 | 213 | async function updateTasksJsonFileForJSONManifest() { 214 | const tasksJson = `.vscode/tasks.json`; 215 | const data = await readFileAsync(tasksJson, "utf8"); 216 | let content = JSON.parse(data); 217 | 218 | content.tasks.forEach(function (task) { 219 | if (task.label.startsWith("Build")) { 220 | task.dependsOn = ["Install"]; 221 | } 222 | if (task.label === "Debug: Outlook Desktop") { 223 | task.script = "start"; 224 | task.dependsOn = ["Check OS", "Install"]; 225 | } 226 | }); 227 | 228 | const checkOSTask = { 229 | label: "Check OS", 230 | type: "shell", 231 | windows: { 232 | command: "echo 'Sideloading in Outlook on Windows is supported'", 233 | }, 234 | linux: { 235 | command: "echo 'Sideloading on Linux is not supported' && exit 1", 236 | }, 237 | osx: { 238 | command: "echo 'Sideloading in Outlook on Mac is not supported' && exit 1", 239 | }, 240 | presentation: { 241 | clear: true, 242 | panel: "dedicated", 243 | }, 244 | }; 245 | 246 | content.tasks.push(checkOSTask); 247 | await writeFileAsync(tasksJson, JSON.stringify(content, null, 2)); 248 | } 249 | 250 | async function updateWebpackConfigForJSONManifest() { 251 | const webPack = `webpack.config.js`; 252 | const webPackContent = await readFileAsync(webPack, "utf8"); 253 | const updatedContent = webPackContent.replace(".xml", ".json"); 254 | await writeFileAsync(webPack, updatedContent); 255 | } 256 | 257 | async function modifyProjectForJSONManifest() { 258 | await updatePackageJsonForJSONManifest(); 259 | await updateWebpackConfigForJSONManifest(); 260 | await updateTasksJsonFileForJSONManifest(); 261 | await deleteXMLManifestRelatedFiles(); 262 | } 263 | 264 | /** 265 | * Modify the project so that it only supports a single host. 266 | * @param host The host to support. 267 | */ 268 | modifyProjectForSingleHost(host).catch((err) => { 269 | console.error(`Error modifying for single host: ${err instanceof Error ? err.message : err}`); 270 | process.exitCode = 1; 271 | }); 272 | 273 | let manifestPath = "manifest.xml"; 274 | 275 | // Uncomment when template supports JSON manifest 276 | // if (host !== "outlook" || manifestType !== "json") { 277 | // Remove things that are only relevant to JSON manifest 278 | deleteJSONManifestRelatedFiles(); 279 | updatePackageJsonForXMLManifest(); 280 | // } else { 281 | // manifestPath = "manifest.json"; 282 | // modifyProjectForJSONManifest().catch((err) => { 283 | // console.error(`Error modifying for JSON manifest: ${err instanceof Error ? err.message : err}`); 284 | // process.exitCode = 1; 285 | // }); 286 | // } 287 | 288 | if (projectName) { 289 | if (!appId) { 290 | appId = "random"; 291 | } 292 | 293 | // Modify the manifest to include the name and id of the project 294 | const cmdLine = `npx office-addin-manifest modify ${manifestPath} -g ${appId} -d "${projectName}"`; 295 | childProcess.exec(cmdLine, (error, stdout) => { 296 | if (error) { 297 | console.error(`Error updating the manifest: ${error}`); 298 | process.exitCode = 1; 299 | } else { 300 | console.log(stdout); 301 | } 302 | }); 303 | } 304 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", 3 | "id": "f2b103f1-1ab1-4e1b-8f0b-072aa3d4e19d", 4 | "manifestVersion": "1.17", 5 | "version": "1.0.0", 6 | "name": { 7 | "short": "Contoso Task Pane Add-in", 8 | "full": "Contoso Task Pane Add-in" 9 | }, 10 | "description": { 11 | "short": "A template to get started.", 12 | "full": "This is the template to get started." 13 | }, 14 | "developer": { 15 | "name": "Contoso", 16 | "websiteUrl": "https://www.contoso.com", 17 | "privacyUrl": "https://www.contoso.com/privacy", 18 | "termsOfUseUrl": "https://www.contoso.com/servicesagreement" 19 | }, 20 | "icons": { 21 | "outline": "assets/outline.png", 22 | "color": "assets/color.png" 23 | }, 24 | "accentColor": "#230201", 25 | "localizationInfo": { 26 | "defaultLanguageTag": "en-us", 27 | "additionalLanguages": [] 28 | }, 29 | "authorization": { 30 | "permissions": { 31 | "resourceSpecific": [ 32 | { 33 | "name": "MailboxItem.Read.User", 34 | "type": "Delegated" 35 | } 36 | ] 37 | } 38 | }, 39 | "validDomains": ["contoso.com"], 40 | "extensions": [ 41 | { 42 | "requirements": { 43 | "scopes": ["mail"], 44 | "capabilities": [ 45 | { "name": "Mailbox", "minVersion": "1.3" } 46 | ] 47 | }, 48 | "runtimes": [ 49 | { 50 | "requirements": { 51 | "capabilities": [ 52 | { "name": "Mailbox", "minVersion": "1.3" } 53 | ] 54 | }, 55 | "id": "TaskPaneRuntime", 56 | "type": "general", 57 | "code": { 58 | "page": "https://localhost:3000/taskpane.html" 59 | }, 60 | "lifetime": "short", 61 | "actions": [ 62 | { 63 | "id": "TaskPaneRuntimeShow", 64 | "type":"openPage", 65 | "pinnable": false, 66 | "view": "dashboard" 67 | } 68 | ] 69 | }, 70 | { 71 | "id": "CommandsRuntime", 72 | "type": "general", 73 | "code": { 74 | "page": "https://localhost:3000/commands.html", 75 | "script": "https://localhost:3000/commands.js" 76 | }, 77 | "lifetime": "short", 78 | "actions": [ 79 | { 80 | "id": "action", 81 | "type": "executeFunction" 82 | } 83 | ] 84 | } 85 | ], 86 | "ribbons": [ 87 | { 88 | "contexts": [ 89 | "mailRead" 90 | ], 91 | "tabs": [ 92 | { 93 | "builtInTabId": "TabDefault", 94 | "groups": [ 95 | { 96 | "id": "msgReadGroup", 97 | "label": "Contoso Add-in", 98 | "icons": [ 99 | { "size": 16, "url": "https://localhost:3000/assets/icon-16.png" }, 100 | { "size": 32, "url": "https://localhost:3000/assets/icon-32.png" }, 101 | { "size": 80, "url": "https://localhost:3000/assets/icon-80.png" } 102 | ], 103 | "controls": [ 104 | { 105 | "id": "msgReadOpenPaneButton", 106 | "type": "button", 107 | "label": "Show Task Pane", 108 | "icons": [ 109 | { "size": 16, "url": "https://localhost:3000/assets/icon-16.png" }, 110 | { "size": 32, "url": "https://localhost:3000/assets/icon-32.png" }, 111 | { "size": 80, "url": "https://localhost:3000/assets/icon-80.png" } 112 | ], 113 | "supertip": { 114 | "title": "Show Task Pane", 115 | "description": "Opens a pane displaying all available properties." 116 | }, 117 | "actionId": "TaskPaneRuntimeShow" 118 | }, 119 | { 120 | "id": "ActionButton", 121 | "type": "button", 122 | "label": "Perform an action", 123 | "icons": [ 124 | { "size": 16, "url": "https://localhost:3000/assets/icon-16.png" }, 125 | { "size": 32, "url": "https://localhost:3000/assets/icon-32.png" }, 126 | { "size": 80, "url": "https://localhost:3000/assets/icon-80.png" } 127 | ], 128 | "supertip": { 129 | "title": "Perform an action", 130 | "description": "Perform an action when clicked." 131 | }, 132 | "actionId": "action" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | ] 139 | } 140 | ] 141 | } 142 | ] 143 | } 144 | -------------------------------------------------------------------------------- /manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | a5f6b5ea-9bf1-4a7c-97ac-1759754457c3 7 | 1.0.0.0 8 | Contoso 9 | en-US 10 | 11 | 12 | 13 | 14 | 15 | 16 | https://www.contoso.com 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ReadWriteDocument 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | <Description resid="GetStarted.Description"/> 51 | <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/> 52 | </GetStarted> 53 | <FunctionFile resid="Commands.Url" /> 54 | <ExtensionPoint xsi:type="PrimaryCommandSurface"> 55 | <OfficeTab id="TabHome"> 56 | <Group id="CommandsGroup"> 57 | <Label resid="CommandsGroup.Label" /> 58 | <Icon> 59 | <bt:Image size="16" resid="Icon.16x16" /> 60 | <bt:Image size="32" resid="Icon.32x32" /> 61 | <bt:Image size="80" resid="Icon.80x80" /> 62 | </Icon> 63 | <Control xsi:type="Button" id="TaskpaneButton"> 64 | <Label resid="TaskpaneButton.Label" /> 65 | <Supertip> 66 | <Title resid="TaskpaneButton.Label" /> 67 | <Description resid="TaskpaneButton.Tooltip" /> 68 | </Supertip> 69 | <Icon> 70 | <bt:Image size="16" resid="Icon.16x16" /> 71 | <bt:Image size="32" resid="Icon.32x32" /> 72 | <bt:Image size="80" resid="Icon.80x80" /> 73 | </Icon> 74 | <Action xsi:type="ShowTaskpane"> 75 | <TaskpaneId>ButtonId1</TaskpaneId> 76 | <SourceLocation resid="Taskpane.Url" /> 77 | </Action> 78 | </Control> 79 | </Group> 80 | </OfficeTab> 81 | </ExtensionPoint> 82 | </DesktopFormFactor> 83 | </Host> 84 | </Hosts> 85 | <Resources> 86 | <bt:Images> 87 | <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/> 88 | <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/> 89 | <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/> 90 | </bt:Images> 91 | <bt:Urls> 92 | <bt:Url id="Functions.Script.Url" DefaultValue="https://localhost:3000/public/functions.js" /> 93 | <bt:Url id="Functions.Metadata.Url" DefaultValue="https://localhost:3000/public/functions.json" /> 94 | <bt:Url id="Functions.Page.Url" DefaultValue="https://localhost:3000/public/functions.html" /> 95 | <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812" /> 96 | <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html" /> 97 | <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html" /> 98 | </bt:Urls> 99 | <bt:ShortStrings> 100 | <bt:String id="Functions.Namespace" DefaultValue="CONTOSO" /> 101 | <bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!" /> 102 | <bt:String id="CommandsGroup.Label" DefaultValue="Commands Group" /> 103 | <bt:String id="TaskpaneButton.Label" DefaultValue="Show Task Pane" /> 104 | </bt:ShortStrings> 105 | <bt:LongStrings> 106 | <bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded successfully. Go to the HOME tab and click the 'Show Task Pane' button to get started." /> 107 | <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Task Pane" /> 108 | </bt:LongStrings> 109 | </Resources> 110 | </VersionOverrides> 111 | </OfficeApp> -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "excel-custom-functions-js", 3 | "version": "0.0.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/OfficeDev/Excel-Custom-Functions-JS.git" 7 | }, 8 | "license": "MIT", 9 | "engines": { 10 | "node": ">=16 <21", 11 | "npm": ">=7 <11" 12 | }, 13 | "scripts": { 14 | "build": "webpack --mode production", 15 | "build:dev": "webpack --mode development", 16 | "convert-to-single-host": "node convertToSingleHost.js", 17 | "dev-server": "webpack serve --mode development", 18 | "lint": "office-addin-lint check", 19 | "lint:fix": "office-addin-lint fix", 20 | "prestart": "npm run build", 21 | "prettier": "office-addin-lint prettier", 22 | "signin": "office-addin-dev-settings m365-account login", 23 | "signout": "office-addin-dev-settings m365-account logout", 24 | "start": "office-addin-debugging start manifest.xml", 25 | "stop": "office-addin-debugging stop manifest.xml", 26 | "test": "npm run test:unit && npm run test:e2e", 27 | "test:e2e": "mocha -r ts-node/register test/end-to-end/tests.ts", 28 | "test:unit": "mocha -r ts-node/register test/unit/*.test.ts", 29 | "validate": "office-addin-manifest validate manifest.xml", 30 | "watch": "webpack --mode development --watch" 31 | }, 32 | "dependencies": { 33 | "core-js": "^3.36.0", 34 | "regenerator-runtime": "^0.14.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.24.0", 38 | "@babel/polyfill": "^7.12.1", 39 | "@babel/preset-env": "^7.25.4", 40 | "@babel/preset-typescript": "^7.23.3", 41 | "@types/custom-functions-runtime": "^1.6.8", 42 | "@types/find-process": "1.2.0", 43 | "@types/mocha": "^10.0.6", 44 | "@types/node": "^20.11.25", 45 | "@types/office-js": "^1.0.377", 46 | "@types/office-runtime": "^1.0.35", 47 | "acorn": "^8.5.0", 48 | "babel-loader": "^9.1.3", 49 | "copy-webpack-plugin": "^12.0.2", 50 | "custom-functions-metadata-plugin": "^2.1.2", 51 | "eslint-plugin-office-addins": "^4.0.3", 52 | "file-loader": "^6.2.0", 53 | "html-loader": "^5.0.0", 54 | "html-webpack-plugin": "^5.6.0", 55 | "mocha": "^11.1.0", 56 | "office-addin-cli": "^2.0.3", 57 | "office-addin-debugging": "^6.0.3", 58 | "office-addin-dev-certs": "^2.0.3", 59 | "office-addin-lint": "^3.0.3", 60 | "office-addin-manifest": "^2.0.3", 61 | "office-addin-mock": "^3.0.3", 62 | "office-addin-prettier-config": "^2.0.1", 63 | "office-addin-test-helpers": "^2.0.3", 64 | "office-addin-test-server": "^2.0.3", 65 | "os-browserify": "^0.3.0", 66 | "process": "^0.11.10", 67 | "request": "^2.88.2", 68 | "source-map-loader": "^5.0.0", 69 | "ts-node": "^10.9.2", 70 | "typescript": "^5.4.2", 71 | "webpack": "^5.95.0", 72 | "webpack-cli": "^5.1.4", 73 | "webpack-dev-server": "5.1.0" 74 | }, 75 | "prettier": "office-addin-prettier-config", 76 | "browserslist": [ 77 | "last 2 versions", 78 | "ie 11" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/commands.html: -------------------------------------------------------------------------------- 1 | <!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. --> 2 | 3 | <!DOCTYPE html> 4 | <html> 5 | 6 | <head> 7 | <meta charset="UTF-8" /> 8 | <meta http-equiv="X-UA-Compatible" content="IE=Edge" /> 9 | 10 | <!-- Office JavaScript API --> 11 | <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script> 12 | </head> 13 | 14 | <body> 15 | 16 | </body> 17 | 18 | </html> 19 | -------------------------------------------------------------------------------- /src/commands/commands.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | /* global Office */ 7 | 8 | Office.onReady(() => { 9 | // If needed, Office.js is ready to be called. 10 | }); 11 | 12 | /** 13 | * Shows a notification when the add-in command is executed. 14 | * @param event 15 | */ 16 | function action(event) { 17 | // Your code goes here 18 | 19 | // Be sure to indicate when the add-in command function is complete. 20 | event.completed(); 21 | } 22 | 23 | // Register the function with Office. 24 | Office.actions.associate("action", action); 25 | -------------------------------------------------------------------------------- /src/functions/functions.html: -------------------------------------------------------------------------------- 1 | <!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. --> 2 | 3 | <!DOCTYPE html> 4 | <html> 5 | 6 | <head> 7 | <meta charset="UTF-8" /> 8 | <meta http-equiv="X-UA-Compatible" content="IE=Edge" /> 9 | <meta http-equiv="Expires" content="0" /> 10 | <title> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/functions/functions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* global console setInterval, clearInterval */ 3 | 4 | /** 5 | * Add two numbers 6 | * @customfunction 7 | * @param {number} first First number 8 | * @param {number} second Second number 9 | * @returns {number} The sum of the two numbers. 10 | */ 11 | function add(first, second) { 12 | return first + second; 13 | } 14 | 15 | /** 16 | * Displays the current time once a second 17 | * @customfunction 18 | * @param {CustomFunctions.StreamingInvocation} invocation Custom function invocation 19 | */ 20 | function clock(invocation) { 21 | const timer = setInterval(() => { 22 | const time = currentTime(); 23 | invocation.setResult(time); 24 | }, 1000); 25 | 26 | invocation.onCanceled = () => { 27 | clearInterval(timer); 28 | }; 29 | } 30 | 31 | /** 32 | * Returns the current time 33 | * @returns {string} String with the current time formatted for the current locale. 34 | */ 35 | function currentTime() { 36 | return new Date().toLocaleTimeString(); 37 | } 38 | 39 | /** 40 | * Increments a value once a second. 41 | * @customfunction 42 | * @param {number} incrementBy Amount to increment 43 | * @param {CustomFunctions.StreamingInvocation} invocation 44 | */ 45 | function increment(incrementBy, invocation) { 46 | let result = 0; 47 | const timer = setInterval(() => { 48 | result += incrementBy; 49 | invocation.setResult(result); 50 | }, 1000); 51 | 52 | invocation.onCanceled = () => { 53 | clearInterval(timer); 54 | }; 55 | } 56 | 57 | /** 58 | * Writes a message to console.log(). 59 | * @customfunction LOG 60 | * @param {string} message String to write. 61 | * @returns String to write. 62 | */ 63 | function logMessage(message) { 64 | console.log(message); 65 | 66 | return message; 67 | } 68 | -------------------------------------------------------------------------------- /src/taskpane/taskpane.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | html, 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | ul { 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | .ms-welcome__header { 20 | padding: 20px; 21 | padding-bottom: 30px; 22 | padding-top: 100px; 23 | display: -webkit-flex; 24 | display: flex; 25 | -webkit-flex-direction: column; 26 | flex-direction: column; 27 | align-items: center; 28 | } 29 | 30 | .ms-welcome__main { 31 | display: -webkit-flex; 32 | display: flex; 33 | -webkit-flex-direction: column; 34 | flex-direction: column; 35 | -webkit-flex-wrap: nowrap; 36 | flex-wrap: nowrap; 37 | -webkit-align-items: center; 38 | align-items: center; 39 | -webkit-flex: 1 0 0; 40 | flex: 1 0 0; 41 | padding: 10px 20px; 42 | } 43 | 44 | .ms-welcome__main > h2 { 45 | width: 100%; 46 | text-align: center; 47 | } 48 | 49 | .ms-welcome__features { 50 | list-style-type: none; 51 | margin-top: 20px; 52 | } 53 | 54 | .ms-welcome__features.ms-List .ms-ListItem { 55 | padding-bottom: 20px; 56 | display: -webkit-flex; 57 | display: flex; 58 | } 59 | 60 | .ms-welcome__features.ms-List .ms-ListItem > .ms-Icon { 61 | margin-right: 10px; 62 | } 63 | 64 | .ms-welcome__action.ms-Button--hero { 65 | margin-top: 30px; 66 | } 67 | 68 | .ms-Button.ms-Button--hero .ms-Button-label { 69 | color: #0078d7; 70 | } 71 | 72 | .ms-Button.ms-Button--hero:hover .ms-Button-label, 73 | .ms-Button.ms-Button--hero:focus .ms-Button-label{ 74 | color: #005a9e; 75 | cursor: pointer; 76 | } 77 | 78 | b { 79 | font-weight: bold; 80 | } -------------------------------------------------------------------------------- /src/taskpane/taskpane.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Contoso Task Pane Add-in 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | Contoso 26 |

Welcome

27 |
28 |
29 |

Please sideload your add-in to see app body.

30 |
31 |
32 |

Discover what Office Add-ins can do for you today!

33 |
    34 |
  • 35 | 36 | Achieve more with Office integration 37 |
  • 38 |
  • 39 | 40 | Unlock features and functionality 41 |
  • 42 |
  • 43 | 44 | Create and visualize like a pro 45 |
  • 46 |
47 |

Modify the source files, then click Run.

48 |
49 | Run 50 |
51 |

52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/taskpane/taskpane.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | /* global console, document, Excel, Office */ 7 | 8 | // The initialize function must be run each time a new page is loaded 9 | Office.onReady(() => { 10 | document.getElementById("sideload-msg").style.display = "none"; 11 | document.getElementById("app-body").style.display = "flex"; 12 | document.getElementById("run").onclick = run; 13 | }); 14 | 15 | export async function run() { 16 | try { 17 | await Excel.run(async (context) => { 18 | /** 19 | * Insert your Excel code here 20 | */ 21 | const range = context.workbook.getSelectedRange(); 22 | 23 | // Read the range address 24 | range.load("address"); 25 | 26 | // Update the fill color 27 | range.format.fill.color = "yellow"; 28 | 29 | await context.sync(); 30 | console.log(`The range address was ${range.address}.`); 31 | }); 32 | } catch (error) { 33 | console.error(error); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/end-to-end/src/debugger-websocket.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { sleep } from "./test-helpers"; 3 | const WebSocket = require("ws"); 4 | const request = require("request"); 5 | 6 | /* global require, console */ 7 | let connectionOpened = false; 8 | let messageId = 0; 9 | const limitOfReconnectTries = 60; 10 | let wsUrl: string | undefined; 11 | 12 | function findUrl(jsonUrl: string): void { 13 | let options = { json: true }; 14 | 15 | request(jsonUrl, options, (error, res, body) => { 16 | if (!error && res.statusCode == 200) { 17 | wsUrl = body[0].webSocketDebuggerUrl; 18 | } 19 | }); 20 | } 21 | 22 | export async function connectToWebsocket(reconnectTry: number = 1): Promise { 23 | // different JS engins used in office use different ports. Allow for either one 24 | let jsonUrl = reconnectTry % 2 == 0 ? "http://127.0.0.1:9223/json" : "http://127.0.0.1:9229/json"; 25 | 26 | while (!wsUrl && reconnectTry < limitOfReconnectTries) { 27 | console.log(`Attaching debugger to '${jsonUrl}'...`); 28 | findUrl(jsonUrl); 29 | reconnectTry++; 30 | await sleep(1000); 31 | } 32 | 33 | return new Promise((resolve) => { 34 | console.log(`Connecting to websocket '${wsUrl}'...`); 35 | const ws = new WebSocket(wsUrl); 36 | 37 | ws.onopen = () => { 38 | console.log("Connection opened"); 39 | connectionOpened = true; 40 | return resolve(ws); 41 | }; 42 | ws.onerror = (err) => { 43 | if (connectionOpened) { 44 | assert.fail(`Websocket error: ${err.message}`); 45 | } 46 | }; 47 | ws.onmessage = (response) => { 48 | assert.strictEqual( 49 | JSON.parse(response.data).error, 50 | undefined, 51 | `Error: ${JSON.parse(response.data).error?.message}` 52 | ); 53 | }; 54 | ws.onclose = async () => { 55 | if (connectionOpened) { 56 | console.log("Closing websocket"); 57 | } else if (reconnectTry < limitOfReconnectTries) { 58 | await sleep(1000); 59 | return resolve(await connectToWebsocket(reconnectTry + 1)); 60 | } else { 61 | return resolve(undefined); 62 | } 63 | }; 64 | }); 65 | } 66 | 67 | export function composeWsMessage(method: string) { 68 | return JSON.stringify({ 69 | id: ++messageId, 70 | method: method, 71 | }); 72 | } 73 | 74 | export async function enableDebugging(ws: WebSocket) { 75 | ws.send(composeWsMessage("Debugger.enable")); 76 | await sleep(1000); 77 | } 78 | 79 | export async function pauseDebugging(ws: WebSocket) { 80 | ws.send(composeWsMessage("Debugger.pause")); 81 | await sleep(1000); 82 | } 83 | 84 | export async function resumeDebugging(ws: WebSocket) { 85 | ws.send(composeWsMessage("Debugger.resume")); 86 | await sleep(1000); 87 | } 88 | -------------------------------------------------------------------------------- /test/end-to-end/src/test-commands.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/end-to-end/src/test-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "ADD": { 4 | "formula": "=CONTOSO.ADD(5,2)", 5 | "result": 7 6 | }, 7 | "CLOCK": { 8 | "formula": "=CONTOSO.CLOCK()", 9 | "result": { 10 | "amString": "AM", 11 | "pmString": "PM", 12 | "timeZoneString": "GMT" 13 | }, 14 | "streaming": true 15 | }, 16 | "INCREMENT": { 17 | "formula": "=CONTOSO.INCREMENT(4)", 18 | "result": "4", 19 | "streaming": true 20 | }, 21 | "LOG": { 22 | "formula": "=CONTOSO.LOG(\"this is a test\")", 23 | "result": "this is a test" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /test/end-to-end/src/test-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from "child_process"; 2 | 3 | /* global process, Excel, setTimeout */ 4 | 5 | export async function closeWorkbook(): Promise { 6 | await sleep(3000); // wait for host to settle 7 | try { 8 | await Excel.run(async (context) => { 9 | // @ts-ignore 10 | context.workbook.close(Excel.CloseBehavior.skipSave); 11 | Promise.resolve(); 12 | }); 13 | } catch (err) { 14 | Promise.reject(`Error on closing workbook: ${err}`); 15 | } 16 | } 17 | 18 | export function addTestResult(testValues: any[], resultName: string, resultValue: any, expectedValue: any) { 19 | var data = {}; 20 | data["expectedValue"] = expectedValue; 21 | data["resultName"] = resultName; 22 | data["resultValue"] = resultValue; 23 | testValues.push(data); 24 | } 25 | 26 | export async function closeDesktopApplication(): Promise { 27 | const processName: string = "Excel"; 28 | 29 | try { 30 | let appClosed: boolean = false; 31 | if (process.platform == "win32") { 32 | const cmdLine = `tskill ${processName}`; 33 | appClosed = await executeCommandLine(cmdLine); 34 | } 35 | 36 | return appClosed; 37 | } catch (err) { 38 | throw new Error(`Unable to kill excel process. ${err}`); 39 | } 40 | } 41 | 42 | async function executeCommandLine(cmdLine: string): Promise { 43 | return new Promise((resolve, reject) => { 44 | childProcess.exec(cmdLine, (error) => { 45 | if (error) { 46 | reject(false); 47 | } else { 48 | resolve(true); 49 | } 50 | }); 51 | }); 52 | } 53 | 54 | export async function sleep(ms: number): Promise { 55 | return new Promise((resolve) => setTimeout(resolve, ms)); 56 | } -------------------------------------------------------------------------------- /test/end-to-end/src/test-taskpane.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Contoso Task Pane Add-in 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | Contoso 26 |

Testing

27 |
28 |
29 |

Please sideload your add-in to see app body.

30 |
31 |
32 |

Discover what Office Add-ins can do for you today!

33 |
    34 |
  • 35 | 36 | Achieve more with Office integration 37 |
  • 38 |
  • 39 | 40 | Unlock features and functionality 41 |
  • 42 |
  • 43 | 44 | Create and visualize like a pro 45 |
  • 46 |
47 |

Modify the source files, then click Run.

48 |
49 | Run 50 |
51 |
52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/end-to-end/src/test-taskpane.ts: -------------------------------------------------------------------------------- 1 | import functionsJsonData from "./test-data.json"; 2 | import { pingTestServer, sendTestResults } from "office-addin-test-helpers"; 3 | import { closeWorkbook, sleep } from "./test-helpers"; 4 | 5 | /* global Office, document, Excel, run, navigator */ 6 | const customFunctionsData = (functionsJsonData).functions; 7 | const port: number = 4201; 8 | let testValues = []; 9 | 10 | Office.onReady(async () => { 11 | document.getElementById("sideload-msg").style.display = "none"; 12 | document.getElementById("app-body").style.display = "flex"; 13 | document.getElementById("run").onclick = run; 14 | addTestResult("UserAgent", navigator.userAgent); 15 | 16 | const testServerResponse: object = await pingTestServer(port); 17 | if (testServerResponse["status"] === 200) { 18 | await runCfTests(); 19 | await sendTestResults(testValues, port); 20 | await closeWorkbook(); 21 | } 22 | }); 23 | 24 | async function runCfTests(): Promise { 25 | // Exercise custom functions 26 | await Excel.run(async (context) => { 27 | for (let key in customFunctionsData) { 28 | const formula: string = customFunctionsData[key].formula; 29 | const range = context.workbook.getSelectedRange(); 30 | range.formulas = [[formula]]; 31 | await context.sync(); 32 | 33 | await sleep(5000); 34 | 35 | // Check to if this is a streaming function 36 | await readCFData(key, customFunctionsData[key].streaming != undefined ? 2 : 1); 37 | } 38 | }); 39 | } 40 | 41 | export async function readCFData(cfName: string, readCount: number): Promise { 42 | await Excel.run(async (context) => { 43 | // if this is a streaming function, we want to capture two values so we can 44 | // validate the function is indeed streaming 45 | for (let i = 0; i < readCount; i++) { 46 | try { 47 | const range = context.workbook.getSelectedRange(); 48 | range.load("values"); 49 | await context.sync(); 50 | 51 | await sleep(5000); 52 | 53 | addTestResult(cfName, range.values[0][0]); 54 | Promise.resolve(); 55 | } catch { 56 | Promise.reject(); 57 | } 58 | } 59 | }); 60 | } 61 | 62 | function addTestResult(resultName: string, resultValue: any) { 63 | var data = {}; 64 | var nameKey = "Name"; 65 | var valueKey = "Value"; 66 | data[nameKey] = resultName; 67 | data[valueKey] = resultValue; 68 | testValues.push(data); 69 | } 70 | -------------------------------------------------------------------------------- /test/end-to-end/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /test/end-to-end/test-manifest-debugging.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | ca968be6-628b-4f14-ba3c-3e614effa9bd 7 | 1.0.0.0 8 | Contoso 9 | en-US 10 | 11 | 12 | 13 | 14 | 15 | 16 | https://www.contoso.com 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ReadWriteDocument 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | <Description resid="GetStarted.Description"/> 51 | <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/> 52 | </GetStarted> 53 | <FunctionFile resid="Commands.Url" /> 54 | <ExtensionPoint xsi:type="PrimaryCommandSurface"> 55 | <OfficeTab id="TabHome"> 56 | <Group id="CommandsGroup"> 57 | <Label resid="CommandsGroup.Label" /> 58 | <Icon> 59 | <bt:Image size="16" resid="Icon.16x16" /> 60 | <bt:Image size="32" resid="Icon.32x32" /> 61 | <bt:Image size="80" resid="Icon.80x80" /> 62 | </Icon> 63 | <Control xsi:type="Button" id="TaskpaneButton"> 64 | <Label resid="TaskpaneButton.Label" /> 65 | <Supertip> 66 | <Title resid="TaskpaneButton.Label" /> 67 | <Description resid="TaskpaneButton.Tooltip" /> 68 | </Supertip> 69 | <Icon> 70 | <bt:Image size="16" resid="Icon.16x16" /> 71 | <bt:Image size="32" resid="Icon.32x32" /> 72 | <bt:Image size="80" resid="Icon.80x80" /> 73 | </Icon> 74 | <Action xsi:type="ShowTaskpane"> 75 | <TaskpaneId>ButtonId1</TaskpaneId> 76 | <SourceLocation resid="Taskpane.Url" /> 77 | </Action> 78 | </Control> 79 | </Group> 80 | </OfficeTab> 81 | </ExtensionPoint> 82 | </DesktopFormFactor> 83 | </Host> 84 | </Hosts> 85 | <Resources> 86 | <bt:Images> 87 | <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3001/assets/icon-16.png"/> 88 | <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3001/assets/icon-32.png"/> 89 | <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3001/assets/icon-80.png"/> 90 | </bt:Images> 91 | <bt:Urls> 92 | <bt:Url id="Functions.Script.Url" DefaultValue="https://localhost:3001/public/functions.js"/> 93 | <bt:Url id="Functions.Metadata.Url" DefaultValue="https://localhost:3001/public/functions.json"/> 94 | <bt:Url id="Functions.Page.Url" DefaultValue="https://localhost:3001/public/functions.html"/> 95 | <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/> 96 | <bt:Url id="Commands.Url" DefaultValue="https://localhost:3001/commands.html"/> 97 | <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3001/taskpane.html"/> 98 | </bt:Urls> 99 | <bt:ShortStrings> 100 | <bt:String id="Functions.Namespace" DefaultValue="CONTOSO"/> 101 | <bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/> 102 | <bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/> 103 | <bt:String id="TaskpaneButton.Label" DefaultValue="Show Task Pane"/> 104 | </bt:ShortStrings> 105 | <bt:LongStrings> 106 | <bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded successfully. Go to the HOME tab and click the 'Show Task Pane' button to get started." /> 107 | <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Task Pane" /> 108 | </bt:LongStrings> 109 | </Resources> 110 | </VersionOverrides> 111 | </OfficeApp> -------------------------------------------------------------------------------- /test/end-to-end/test-manifest.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" 3 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 | xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" 5 | xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp"> 6 | <Id>ca968be6-628b-4f14-ba3c-3e614effa9bd</Id> 7 | <Version>1.0.0.0</Version> 8 | <ProviderName>Contoso</ProviderName> 9 | <DefaultLocale>en-US</DefaultLocale> 10 | <DisplayName DefaultValue="Excel Custom Functions"/> 11 | <Description DefaultValue="Write your own Excel functions in TypeScript."/> 12 | <IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/> 13 | <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/> 14 | <SupportUrl DefaultValue="https://www.contoso.com/help"/> 15 | <AppDomains> 16 | <AppDomain>https://www.contoso.com</AppDomain> 17 | </AppDomains> 18 | <Hosts> 19 | <Host Name="Workbook"/> 20 | </Hosts> 21 | <Requirements> 22 | <Sets DefaultMinVersion="1.1"> 23 | <Set Name="CustomFunctionsRuntime" MinVersion="1.1"/> 24 | </Sets> 25 | </Requirements> 26 | <DefaultSettings> 27 | <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/> 28 | </DefaultSettings> 29 | <Permissions>ReadWriteDocument</Permissions> 30 | <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0"> 31 | <Hosts> 32 | <Host xsi:type="Workbook"> 33 | <AllFormFactors> 34 | <ExtensionPoint xsi:type="CustomFunctions"> 35 | <Script> 36 | <SourceLocation resid="Functions.Script.Url"/> 37 | </Script> 38 | <Page> 39 | <SourceLocation resid="Functions.Page.Url"/> 40 | </Page> 41 | <Metadata> 42 | <SourceLocation resid="Functions.Metadata.Url"/> 43 | </Metadata> 44 | <Namespace resid="Functions.Namespace"/> 45 | </ExtensionPoint> 46 | </AllFormFactors> 47 | </Host> 48 | </Hosts> 49 | <Resources> 50 | <bt:Images> 51 | <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/> 52 | <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/> 53 | <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/> 54 | </bt:Images> 55 | <bt:Urls> 56 | <bt:Url id="Functions.Script.Url" DefaultValue="https://localhost:3000/public/functions.js"/> 57 | <bt:Url id="Functions.Metadata.Url" DefaultValue="https://localhost:3000/public/functions.json"/> 58 | <bt:Url id="Functions.Page.Url" DefaultValue="https://localhost:3000/public/functions.html"/> 59 | <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/> 60 | <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/> 61 | <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/> 62 | </bt:Urls> 63 | <bt:ShortStrings> 64 | <bt:String id="Functions.Namespace" DefaultValue="CONTOSO"/> 65 | <bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/> 66 | <bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/> 67 | <bt:String id="TaskpaneButton.Label" DefaultValue="Show Task Pane"/> 68 | </bt:ShortStrings> 69 | <bt:LongStrings> 70 | <bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded successfully. Go to the HOME tab and click the 'Show Task Pane' button to get started."/> 71 | <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Task Pane"/> 72 | </bt:LongStrings> 73 | </Resources> 74 | </VersionOverrides> 75 | </OfficeApp> -------------------------------------------------------------------------------- /test/end-to-end/tests.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as fs from "fs"; 3 | import { parseNumber } from "office-addin-cli"; 4 | import { AppType, startDebugging, stopDebugging } from "office-addin-debugging"; 5 | import { toOfficeApp } from "office-addin-manifest"; 6 | import { pingTestServer } from "office-addin-test-helpers"; 7 | import { closeDesktopApplication } from "./src/test-helpers"; 8 | import { connectToWebsocket, enableDebugging, pauseDebugging } from "./src/debugger-websocket"; 9 | import * as officeAddinTestServer from "office-addin-test-server"; 10 | import * as path from "path"; 11 | 12 | /* global process, describe, before, it, after, console */ 13 | const host: string = "excel"; 14 | const manifestPath = path.resolve(`${process.cwd()}/test/end-to-end/test-manifest.xml`); 15 | const manifestPathDebugging = path.resolve(`${process.cwd()}/test/end-to-end/test-manifest-debugging.xml`); 16 | const port: number = 4201; 17 | const testDataFile: string = `${process.cwd()}/test/end-to-end/src/test-data.json`; 18 | const testJsonData = JSON.parse(fs.readFileSync(testDataFile).toString()); 19 | const testServer = new officeAddinTestServer.TestServer(port); 20 | let testValues: any = []; 21 | 22 | describe("Test Excel Custom Functions", function () { 23 | describe("UI Tests", function () { 24 | before(`Setup test environment and sideload ${host}`, async function () { 25 | this.timeout(0); 26 | // Start test server and ping to ensure it's started 27 | const testServerStarted = await testServer.startTestServer(true /* mochaTest */); 28 | const serverResponse = await pingTestServer(port); 29 | assert.strictEqual(serverResponse["status"], 200); 30 | assert.strictEqual(testServerStarted, true); 31 | 32 | // Call startDebugging to start dev-server and sideload 33 | const devServerCmd = `npm run dev-server -- --config ./test/end-to-end/webpack.config.js`; 34 | const devServerPort = parseNumber(process.env.npm_package_config_dev_server_port || 3000); 35 | const options = { 36 | appType: AppType.Desktop, 37 | app: toOfficeApp(host), 38 | devServerCommandLine: devServerCmd, 39 | devServerPort: devServerPort, 40 | enableDebugging: false, 41 | }; 42 | await startDebugging(manifestPath, options); 43 | }); 44 | describe("Get test results for custom functions and validate results", function () { 45 | it("should get results from the taskpane application", async function () { 46 | this.timeout(0); 47 | // Expecting six result values + user agent 48 | testValues = await testServer.getTestResults(); 49 | console.log(`User Agent: ${testValues[0].Value}`); 50 | assert.strictEqual(testValues.length, 7); 51 | }); 52 | it("ADD function should return expected value", async function () { 53 | assert.strictEqual(testJsonData.functions.ADD.result, testValues[1].Value); 54 | }); 55 | it("CLOCK function should return expected value", async function () { 56 | // Check that captured values are different to ensure the function is streaming 57 | assert.notStrictEqual(testValues[2].Value, testValues[3].Value); 58 | // Check if the returned string contains 'AM', 'PM', or 'GMT', indicating it's a time-stamp 59 | assert.strictEqual( 60 | testValues[2].Value.includes(testJsonData.functions.CLOCK.result.amString) || 61 | testValues[2].Value.includes(testJsonData.functions.CLOCK.result.pmString) || 62 | testValues[2].Value.includes(testJsonData.functions.CLOCK.result.timeZoneString), 63 | true, 64 | "Found timestamp indicator string in first value '" + testValues[2].Value + "'" 65 | ); 66 | assert.strictEqual( 67 | testValues[3].Value.includes(testJsonData.functions.CLOCK.result.amString) || 68 | testValues[3].Value.includes(testJsonData.functions.CLOCK.result.pmString) || 69 | testValues[3].Value.includes(testJsonData.functions.CLOCK.result.timeZoneString), 70 | true, 71 | "Found timestamp indicator string in second value '" + testValues[3].Value + "'" 72 | ); 73 | }); 74 | it("INCREMENT function should return expected value", async function () { 75 | // Check that captured values are different to ensure the function is streaming 76 | assert.notStrictEqual(testValues[3].Value, testValues[4].Value); 77 | // Check to see that both captured streaming values are divisible by 4 78 | assert.strictEqual(0, testValues[4].Value % testJsonData.functions.INCREMENT.result); 79 | assert.strictEqual(0, testValues[5].Value % testJsonData.functions.INCREMENT.result); 80 | }); 81 | it("LOG function should return expected value", async function () { 82 | assert.strictEqual(testJsonData.functions.LOG.result, testValues[6].Value); 83 | }); 84 | }); 85 | after("Teardown test environment", async function () { 86 | this.timeout(0); 87 | // Stop the test server 88 | const stopTestServer = await testServer.stopTestServer(); 89 | assert.strictEqual(stopTestServer, true); 90 | 91 | // Close excel 92 | const applicationClosed = await closeDesktopApplication(); 93 | assert.strictEqual(applicationClosed, true); 94 | 95 | // Unregister the add-in 96 | await stopDebugging(manifestPath); 97 | }); 98 | }); 99 | describe("Debugger Tests", function () { 100 | before(`Setup test environment and sideload ${host}`, async function () { 101 | this.timeout(0); 102 | // Call startDebugging to start dev-server and sideload 103 | const devServerCmd: string = `npm run dev-server -- --config ./test/end-to-end/webpack.config.js --env testType=debugger`; 104 | const devServerPort: number = 3001; 105 | const options = { 106 | appType: AppType.Desktop, 107 | app: toOfficeApp(host), 108 | devServerCommandLine: devServerCmd, 109 | devServerPort: devServerPort, 110 | enableDebugging: true, 111 | }; 112 | await startDebugging(manifestPathDebugging, options); 113 | }); 114 | describe("Test Debugger", function () { 115 | let ws: WebSocket; 116 | before("Open websocket connection to Debugger", async function () { 117 | this.timeout(60 * 1000); 118 | ws = await connectToWebsocket(); 119 | assert.notStrictEqual(ws, undefined, "Unable to connect to the websocket."); 120 | }); 121 | it("enable debugging", async function () { 122 | await enableDebugging(ws); 123 | }); 124 | it("pause debugging", async function () { 125 | await pauseDebugging(ws); 126 | }); 127 | after("Close websocket connection", async function () { 128 | ws.close(); 129 | }); 130 | }); 131 | after("Teardown test environment", async function () { 132 | this.timeout(0); 133 | // Close excel 134 | const applicationClosed = await closeDesktopApplication(); 135 | assert.strictEqual(applicationClosed, true); 136 | 137 | // Unregister the add-in 138 | await stopDebugging(manifestPathDebugging); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/end-to-end/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const devCerts = require("office-addin-dev-certs"); 4 | const CustomFunctionsMetadataPlugin = require("custom-functions-metadata-plugin"); 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 7 | const path = require("path"); 8 | 9 | /* global require, module, process, __dirname */ 10 | 11 | async function getHttpsOptions() { 12 | const httpsOptions = await devCerts.getHttpsServerOptions(); 13 | return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert }; 14 | } 15 | 16 | module.exports = async (env, options) => { 17 | const debuggingTest = env.testType === "debugger"; 18 | const config = { 19 | devtool: "source-map", 20 | entry: { 21 | polyfill: ["core-js/stable", "regenerator-runtime/runtime"], 22 | commands: "./src/commands/commands.js", 23 | taskpane: ["./test/end-to-end/src/test-taskpane.ts", "./test/end-to-end/src/test-taskpane.html"], 24 | functions: "./src/functions/functions.js", 25 | }, 26 | output: { 27 | path: path.resolve(__dirname, "testBuild"), 28 | clean: true, 29 | }, 30 | resolve: { 31 | extensions: [".ts", ".tsx", ".html", ".js"], 32 | fallback: { 33 | child_process: false, 34 | fs: false, 35 | os: require.resolve("os-browserify/browser"), 36 | }, 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.ts$/, 42 | exclude: /node_modules/, 43 | use: { 44 | loader: "babel-loader", 45 | options: { 46 | presets: ["@babel/preset-typescript"], 47 | }, 48 | }, 49 | }, 50 | { 51 | test: /\.js$/, 52 | exclude: /node_modules/, 53 | use: { 54 | loader: "babel-loader", 55 | options: { 56 | presets: ["@babel/preset-env"], 57 | }, 58 | }, 59 | }, 60 | { 61 | test: /\.html$/, 62 | exclude: /node_modules/, 63 | use: "html-loader", 64 | }, 65 | { 66 | test: /\.(png|jpg|jpeg|gif|ico)$/, 67 | type: "asset/resource", 68 | generator: { 69 | filename: "assets/[name][ext][query]", 70 | }, 71 | }, 72 | ], 73 | }, 74 | plugins: [ 75 | new CustomFunctionsMetadataPlugin({ 76 | output: "functions.json", 77 | input: "./src/functions/functions.js", 78 | }), 79 | new HtmlWebpackPlugin({ 80 | filename: "functions.html", 81 | template: "./src/functions/functions.html", 82 | chunks: ["polyfill", "functions"], 83 | }), 84 | new HtmlWebpackPlugin({ 85 | filename: "taskpane.html", 86 | template: "./test/end-to-end/src/test-taskpane.html", 87 | chunks: ["polyfill", "taskpane"], 88 | }), 89 | new HtmlWebpackPlugin({ 90 | filename: "commands.html", 91 | template: "./test/end-to-end/src/test-commands.html", 92 | chunks: ["polyfill", "commands"], 93 | }), 94 | new CopyWebpackPlugin({ 95 | patterns: [ 96 | { 97 | from: "assets/*", 98 | to: "assets/[name][ext][query]", 99 | }, 100 | ], 101 | }), 102 | ], 103 | devServer: { 104 | static: { 105 | directory: path.resolve("./", "dist"), 106 | publicPath: "/public", 107 | }, 108 | headers: { 109 | "Access-Control-Allow-Origin": "*", 110 | }, 111 | server: { 112 | type: "https", 113 | options: env.WEBPACK_BUILD || options.https !== undefined ? options.https : await getHttpsOptions(), 114 | }, 115 | port: debuggingTest ? 3001 : process.env.npm_package_config_dev_server_port || 3000, 116 | }, 117 | }; 118 | 119 | return config; 120 | }; 121 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "experimentalDecorators": true, 6 | "jsx": "react", 7 | "noEmitOnError": true, 8 | "outDir": "lib", 9 | "sourceMap": true, 10 | "target": "es5", 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "dist", 15 | "lib", 16 | "lib-amd" 17 | ] 18 | } -------------------------------------------------------------------------------- /test/unit/excel.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import "mocha"; 3 | import { OfficeMockObject } from "office-addin-mock"; 4 | 5 | /* global describe, global, it, require */ 6 | 7 | const ExcelMockData = { 8 | context: { 9 | workbook: { 10 | range: { 11 | address: "G4", 12 | format: { 13 | fill: {}, 14 | }, 15 | }, 16 | getSelectedRange: function () { 17 | return this.range; 18 | }, 19 | }, 20 | }, 21 | run: async function (callback) { 22 | await callback(this.context); 23 | }, 24 | }; 25 | 26 | const OfficeMockData = { 27 | onReady: async function () {}, 28 | }; 29 | 30 | describe("Excel", function () { 31 | it("Run", async function () { 32 | const excelMock: OfficeMockObject = new OfficeMockObject(ExcelMockData); // Mocking the host specific namespace 33 | global.Excel = excelMock as any; 34 | global.Office = new OfficeMockObject(OfficeMockData) as any; // Mocking the common office-js namespace 35 | 36 | const { run } = require("../../src/taskpane/taskpane"); 37 | await run(); 38 | 39 | assert.strictEqual(excelMock.context.workbook.range.format.fill.color, "yellow"); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "jsx": "react", 8 | "noEmitOnError": true, 9 | "outDir": "lib", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "lib": [ 13 | "es2015", 14 | "dom" 15 | ] 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "dist", 20 | "lib", 21 | "lib-amd" 22 | ], 23 | "ts-node": { 24 | "files": true 25 | } 26 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const devCerts = require("office-addin-dev-certs"); 4 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 5 | const CustomFunctionsMetadataPlugin = require("custom-functions-metadata-plugin"); 6 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 7 | const path = require("path"); 8 | 9 | const urlDev = "https://localhost:3000/"; 10 | const urlProd = "https://www.contoso.com/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION 11 | 12 | /* global require, module, process, __dirname */ 13 | 14 | async function getHttpsOptions() { 15 | const httpsOptions = await devCerts.getHttpsServerOptions(); 16 | return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert }; 17 | } 18 | 19 | module.exports = async (env, options) => { 20 | const dev = options.mode === "development"; 21 | const config = { 22 | devtool: "source-map", 23 | entry: { 24 | polyfill: ["core-js/stable", "regenerator-runtime/runtime"], 25 | taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"], 26 | commands: "./src/commands/commands.js", 27 | functions: "./src/functions/functions.js", 28 | }, 29 | output: { 30 | clean: true, 31 | }, 32 | resolve: { 33 | extensions: [".html", ".js"], 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.js$/, 39 | exclude: /node_modules/, 40 | use: { 41 | loader: "babel-loader", 42 | }, 43 | }, 44 | { 45 | test: /\.html$/, 46 | exclude: /node_modules/, 47 | use: "html-loader", 48 | }, 49 | { 50 | test: /\.(png|jpg|jpeg|gif|ico)$/, 51 | type: "asset/resource", 52 | generator: { 53 | filename: "assets/[name][ext][query]", 54 | }, 55 | }, 56 | ], 57 | }, 58 | plugins: [ 59 | new CustomFunctionsMetadataPlugin({ 60 | output: "functions.json", 61 | input: "./src/functions/functions.js", 62 | }), 63 | new HtmlWebpackPlugin({ 64 | filename: "functions.html", 65 | template: "./src/functions/functions.html", 66 | chunks: ["polyfill", "functions"], 67 | }), 68 | new HtmlWebpackPlugin({ 69 | filename: "taskpane.html", 70 | template: "./src/taskpane/taskpane.html", 71 | chunks: ["polyfill", "taskpane"], 72 | }), 73 | new CopyWebpackPlugin({ 74 | patterns: [ 75 | { 76 | from: "assets/*", 77 | to: "assets/[name][ext][query]", 78 | }, 79 | { 80 | from: "manifest*.xml", 81 | to: "[name]" + "[ext]", 82 | transform(content) { 83 | if (dev) { 84 | return content; 85 | } else { 86 | return content.toString().replace(new RegExp(urlDev + "(?:public/)?", "g"), urlProd); 87 | } 88 | }, 89 | }, 90 | ], 91 | }), 92 | new HtmlWebpackPlugin({ 93 | filename: "commands.html", 94 | template: "./src/commands/commands.html", 95 | chunks: ["polyfill", "commands"], 96 | }), 97 | ], 98 | devServer: { 99 | static: { 100 | directory: path.join(__dirname, "dist"), 101 | publicPath: "/public", 102 | }, 103 | headers: { 104 | "Access-Control-Allow-Origin": "*", 105 | }, 106 | server: { 107 | type: "https", 108 | options: env.WEBPACK_BUILD || options.https !== undefined ? options.https : await getHttpsOptions(), 109 | }, 110 | port: process.env.npm_package_config_dev_server_port || 3000, 111 | }, 112 | }; 113 | 114 | return config; 115 | }; 116 | --------------------------------------------------------------------------------