├── CODEOWNERS ├── .yarnrc.yml ├── .npmignore ├── .prettierignore ├── .stylelintignore ├── .eslintignore ├── src ├── public │ └── favicon.ico ├── __tests__ │ ├── tests │ │ ├── platform-utils │ │ │ ├── widgets │ │ │ │ ├── get-not-found.playwright.ts │ │ │ │ ├── getLoader.playwright.ts │ │ │ │ └── get.playwright.ts │ │ │ ├── context.playwright.ts │ │ │ ├── experience.playwright.ts │ │ │ ├── localStorage.playwright.ts │ │ │ ├── sessionStorage.playwright.ts │ │ │ ├── appLoadTime.playwright.ts │ │ │ └── eventbus.playwright.ts │ │ └── shell │ │ │ ├── kitchensink.playwright.ts │ │ │ ├── import-platform-props.playwright.ts │ │ │ ├── retry-critical-imports.playwright.ts │ │ │ ├── preload │ │ │ ├── libraries.playwright.ts │ │ │ ├── client-side-widgets-get.playwright.ts │ │ │ ├── fetch-wsk.playwright.ts │ │ │ ├── api-get.playwright.ts │ │ │ └── shell-and-plugin.playwright.ts │ │ │ ├── csp │ │ │ ├── csp-isolation.playwright.ts │ │ │ └── csp-runtime-override.playwright.ts │ │ │ ├── loading-spinner.playwright.ts │ │ │ ├── browser-not-supported.playwright.ts │ │ │ ├── page-not-found.playwright.ts │ │ │ ├── overrides │ │ │ ├── meta-tags.playwright.ts │ │ │ ├── sanitize-query-params.playwright.ts │ │ │ ├── runtime-config-overrides.playwright.ts │ │ │ ├── import-map-overrides.playwright.ts │ │ │ └── widget-url-overrides.playwright.ts │ │ │ ├── sessionIdCookie.playwright.ts │ │ │ ├── validate-headers-response.playwright.ts │ │ │ └── versions-contract-stablity.playwright.ts │ └── test-utils │ │ ├── setUserAgentForWidgetUrlOverride.ts │ │ ├── localStorage.ts │ │ ├── importMapOverridesUi.ts │ │ └── envs.ts ├── server │ ├── exceptions │ │ ├── errorCodes.ts │ │ ├── HttpException.ts │ │ └── APIException.ts │ ├── types │ │ └── APIErrorResponse.ts │ └── middlewares │ │ ├── error.middleware.ts │ │ └── error-template.ts ├── shell │ ├── components │ │ ├── Loader.tsx │ │ └── Error.tsx │ ├── index.ts │ ├── logger │ │ └── index.ts │ ├── types │ │ └── utils.ts │ ├── utils │ │ └── init-service-worker.ts │ └── main.tsx ├── lib │ └── router.ts ├── configs │ ├── env.ts │ ├── ecosystem-configs.ts │ ├── critical-libs.ts │ └── widgetConfigs.ts ├── csp-configs.ts ├── server.ts └── sw.ts ├── bundle ├── client │ ├── build.js │ ├── sw.js │ ├── shell-types.js │ ├── dev.js │ ├── utils.js │ └── prod.js └── server │ ├── build.js │ ├── dev.js │ └── common.js ├── .lintstagedrc.json ├── .github ├── actions │ └── common-ci │ │ ├── build │ │ └── action.yml │ │ ├── setup │ │ └── action.yml │ │ └── test │ │ └── action.yml └── workflows │ └── ci.yml ├── .stylelintrc.json ├── tsconfig-sw.json ├── tsconfig-shell.types.json ├── .gitignore ├── .changeset ├── config.json └── README.md ├── render.yaml ├── .prettierrc.json ├── tsconfig.server.json ├── app.json ├── .eslintrc.json ├── playwright.config.ts ├── CHANGELOG.md ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @docusign/1fe 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | bundle 3 | .github 4 | .yarn 5 | playwright.config.ts 6 | test-results 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache 7 | bundle -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docusign/1fe-starter-app/main/src/public/favicon.ico -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/widgets/get-not-found.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Add input box to test 2 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/widgets/getLoader.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Add skeleton loader test 2 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/kitchensink.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Add back tests when bathtub is complete 2 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/import-platform-props.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Add this test back when imports are added 2 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/retry-critical-imports.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe][post-mvp]: add back critical import test 2 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/preload/libraries.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Add this test when we have decided cdn hosting for libraries 2 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/csp/csp-isolation.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Add csp isolation test back after runtime configs are available 2 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/preload/client-side-widgets-get.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Add this test when we have runtime configurations example. 2 | -------------------------------------------------------------------------------- /bundle/client/build.js: -------------------------------------------------------------------------------- 1 | const getProdConfig = require('./prod'); 2 | 3 | const buildConfig = getProdConfig({}); 4 | 5 | module.exports = buildConfig; 6 | -------------------------------------------------------------------------------- /src/server/exceptions/errorCodes.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCodes { 2 | // AUTH RELATED ERROR 3 | AuthorizationError = 100, 4 | UserInfoError = 101, 5 | } 6 | -------------------------------------------------------------------------------- /src/shell/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Loader = () => ( 4 |

Starter App Loading...

5 | ); 6 | -------------------------------------------------------------------------------- /src/shell/index.ts: -------------------------------------------------------------------------------- 1 | export type { PlatformPropsWithCustomUtils } from './types/utils'; 2 | 3 | export {}; // Exporting something to ensure this file is treated as a module 4 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": "eslint --fix --cache", 3 | "*.css": "stylelint --fix", 4 | "*.{js,jsx,ts,tsx,css,html,json,md,mdx}": "prettier --write" 5 | } 6 | -------------------------------------------------------------------------------- /.github/actions/common-ci/build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | description: 'Build the application' 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - run: yarn build 8 | shell: bash 9 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"], 3 | "rules": { 4 | "selector-class-pattern": null, 5 | "keyframes-name-pattern": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/shell/logger/index.ts: -------------------------------------------------------------------------------- 1 | export const shellLogger = { 2 | log: (logObject: any) => { 3 | console.log(logObject); 4 | }, 5 | error: (logObject: any) => { 6 | console.error(logObject); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.github/actions/common-ci/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Environment" 2 | description: "Install dependencies" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - run: yarn install --no-immutable 8 | shell: bash 9 | -------------------------------------------------------------------------------- /src/lib/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | router.get('/hello', async (_req, res) => { 6 | res.status(200).json({ message: 'Hello World!' }); 7 | }); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /tsconfig-sw.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "composite": true, 5 | "target": "es2020", 6 | "module": "es2020", 7 | "moduleResolution": "Node", 8 | "outDir": "dist", 9 | "lib": ["WebWorker", "ES2022"] 10 | }, 11 | "include": ["src/sw.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/server/exceptions/HttpException.ts: -------------------------------------------------------------------------------- 1 | export class HttpException extends Error { 2 | public status: number; 3 | 4 | public message: string; 5 | 6 | constructor(status: number, message: string) { 7 | super(message); 8 | this.status = status; 9 | this.message = message; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig-shell.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "strict": true, 7 | "target": "ESNext", 8 | "module": "ESNext", 9 | "moduleResolution": "Node" 10 | }, 11 | "include": ["src/shell/index.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache 7 | # Yarn 8 | .pnp.* 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/sdks 14 | !.yarn/versions 15 | 16 | # Playwright 17 | test-results/ 18 | 19 | # local development 20 | .env 21 | .npmrc 22 | -------------------------------------------------------------------------------- /bundle/server/build.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const { commonEsbuild } = require('./common'); 3 | // const { nodeConsoleLogger } = require('@1ds/helpers/node'); 4 | 5 | (async () => { 6 | const result = await esbuild.build(commonEsbuild); 7 | console.log(`[SERVER][BUILD] ${JSON.stringify(result)}`); 8 | })(); 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: my-1fe-app 4 | env: node 5 | buildCommand: yarn install && yarn build 6 | startCommand: yarn start 7 | plan: free # Or 'starter', 'standard', etc. 8 | envVars: 9 | - key: NODE_ENV 10 | value: production 11 | - key: BUILD_NUMBER 12 | sync: false 13 | -------------------------------------------------------------------------------- /.github/actions/common-ci/test/action.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | description: "Run playwright tests" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - run: yarn playwright install --with-deps 8 | shell: bash 9 | 10 | - run: yarn test:playwright 11 | shell: bash 12 | env: 13 | AZURE_APPCONFIG_CONNECTION_STRING: "" 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "endOfLine": "lf", 8 | "printWidth": 80, 9 | "overrides": [ 10 | { 11 | "files": ["*.yaml", "*.yml"], 12 | "options": { 13 | "singleQuote": false 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/shell/types/utils.ts: -------------------------------------------------------------------------------- 1 | import { PlatformPropsType } from '@1fe/shell'; 2 | 3 | export type CustomExampleUtils = { 4 | initializeLogger: (widgetId: string) => { 5 | logger: { 6 | log: (message: string) => void; 7 | error: (message: string) => void; 8 | }; 9 | }; 10 | }; 11 | 12 | export type PlatformPropsWithCustomUtils = PlatformPropsType; -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "ES2022", 6 | "module": "commonjs", 7 | "noEmit": false 8 | }, 9 | "include": [ 10 | "src/**/*.ts", 11 | "src/**/*.tsx", 12 | "src/**/*.d.ts", 13 | "src/**/*.json" 14 | ], 15 | "exclude": ["src/shell", "src/sw.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "1fe starter app", 3 | "description": "A sample 1fe app", 4 | "keywords": ["1fe", "microfrontends", "micro", "frontend"], 5 | "env": { 6 | "BUILD_NUMBER": { 7 | "description": "A build number for the 1fe shell bundle. This is used as the folder name to find your shell bundle in your CDN" 8 | }, 9 | "NODE_ENV": "production", 10 | "DEPLOY_SOURCE": "heroku" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/server/types/APIErrorResponse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Standard response for errors returned from API endpoints 3 | */ 4 | export type APIErrorResponse = { 5 | /** 6 | * Error codes that are informative to the client but do not expose internal details 7 | * of the API, specifically with auth. 8 | */ 9 | errorCode: number; 10 | /** 11 | * Error message to help the client resolve the issue. 12 | */ 13 | errorMessage: string; 14 | }; 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 12, 15 | "sourceType": "module" 16 | }, 17 | "ignorePatterns": ["bundle/**/*"], 18 | "plugins": ["@typescript-eslint"], 19 | "rules": {} 20 | } 21 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | reporter: process.env.CI ? 'github' : 'list', 5 | webServer: { 6 | command: 'yarn dev', 7 | url: 'http://localhost:3001', 8 | reuseExistingServer: !process.env.CI, 9 | stdout: 'ignore', 10 | stderr: 'pipe', 11 | timeout: 30 * 1000, 12 | }, 13 | use: { 14 | testIdAttribute: 'data-qa', 15 | }, 16 | testDir: './src/__tests__/tests', 17 | testMatch: '**/*.playwright.ts', 18 | }); 19 | -------------------------------------------------------------------------------- /src/configs/env.ts: -------------------------------------------------------------------------------- 1 | export const ExampleHostedEnvironments = { 2 | integration: 'integration', 3 | production: 'production', 4 | }; 5 | 6 | export const ENVIRONMENT: string = process.env.NODE_ENV === 'development' ? ExampleHostedEnvironments.integration : (process.env.NODE_ENV || ExampleHostedEnvironments.production); 7 | export const isLocal = process.env.NODE_ENV === 'development'; 8 | export const isProduction = ExampleHostedEnvironments.production === ENVIRONMENT; 9 | export const SERVER_BUILD_NUMBER = process.env.BUILD_NUMBER || 'local'; -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /src/shell/utils/init-service-worker.ts: -------------------------------------------------------------------------------- 1 | export function initServiceWorker() { 2 | window.addEventListener('load', () => { 3 | const swUrl = `/sw.js`; 4 | 5 | window.navigator.serviceWorker 6 | .register(swUrl) 7 | .then((registration) => { 8 | console.log('SW registered: ', registration); 9 | }) 10 | .catch((registrationError) => { 11 | console.error( 12 | '[SW] Service Worker registration failed: ', 13 | registrationError, 14 | ); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/configs/ecosystem-configs.ts: -------------------------------------------------------------------------------- 1 | import { OneFEConfigManagement } from '@1fe/server'; 2 | import { ENVIRONMENT } from './env'; 3 | import { getWidgetVersions } from './widgetConfigs'; 4 | 5 | export const configManagement: OneFEConfigManagement = { 6 | widgetVersions: { 7 | get: getWidgetVersions, 8 | }, 9 | libraryVersions: { 10 | url: `https://1fe-a.akamaihd.net/${ENVIRONMENT}/configs/lib-versions.json`, 11 | }, 12 | dynamicConfigs: { 13 | url: `https://1fe-a.akamaihd.net/${ENVIRONMENT}/configs/live.json`, 14 | }, 15 | refreshMs: 30 * 1000, 16 | }; 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @1fe/starter-app 2 | 3 | ## 0.0.4 4 | 5 | ### Patch Changes 6 | 7 | - cccb035: Test OIDC publication 8 | 9 | ## 0.0.3 10 | 11 | ### Patch Changes 12 | 13 | - eddec33: chore: manual patch release to test publishing pipeline 14 | 15 | This changeset triggers a patch version bump to test the complete 16 | CI/CD publishing pipeline with all the recent fixes applied. 17 | 18 | ## 0.0.2 19 | 20 | ### Patch Changes 21 | 22 | - 98779f2: docs: update documentation URLs from 1fe.com/getting-started/installation to 1fe.com/start-here 23 | - 965caa6: feat: initial publish of starter apps package 24 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/widgets/get.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | // TODO[1fe][milestone-1]: Add pinned and variant tests 4 | // TODO[1fe]: Add nested widget test 5 | test('should load widget', async ({ page }) => { 6 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 7 | 8 | await page.click('button[data-qa="utils.widgets.get.btn"]'); 9 | 10 | const childContainer = await page.locator( 11 | 'div[data-qa="utils.widgets.get.result.container"]', 12 | ); 13 | 14 | await expect(childContainer).toContainText( 15 | 'My component from app2 is mounted', 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/test-utils/setUserAgentForWidgetUrlOverride.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | 3 | /** 4 | * To prevent XSS with widget_url_override, we check user agent for existence 5 | * of secret substring values. This should be used whenever widget_url_overrides is used 6 | * in e2e tests 7 | * @param page 8 | * @returns 9 | */ 10 | export const setUserAgentForWidgetUrlOverride = async ( 11 | page: Page, 12 | ): Promise => { 13 | await page.route('**/*', (route, request) => { 14 | const headers = { 15 | ...request.headers(), 16 | 'user-agent': '1fe.developer', 17 | }; 18 | route.continue({ headers }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/loading-spinner.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('Circular spinner should be displayed while widget is loading @e2e @visual', async ({ 4 | page, 5 | }) => { 6 | const loader = page.locator('p[data-qa="app.custom.loader"]'); 7 | await page.route( 8 | /.*\/widget-starter-kit\/[0-9.]+\/js\/1fe-bundle\.js/, 9 | (route) => { 10 | setTimeout(() => { 11 | route.continue(); 12 | }, 10000); 13 | }, 14 | ); 15 | await page.goto('http://localhost:3001/widget-starter-kit/utils', { 16 | waitUntil: 'domcontentloaded', 17 | }); 18 | 19 | await expect(loader).toBeAttached(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/context.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('Context functions in @internal/generic-child-widget', async ({ 4 | page, 5 | }) => { 6 | // Navigate to the app 7 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 8 | 9 | const resultElement = page.getByTestId('wsk.context.result.container'); 10 | 11 | await expect(resultElement).toHaveText(''); 12 | 13 | await page.getByTestId('utils.context.self.btn').click(); 14 | 15 | await page.waitForTimeout(100); 16 | 17 | const selfContent = JSON.parse((await resultElement.textContent()) || '{}'); 18 | expect(selfContent.widgetId).toContain('@1fe/widget-starter-kit'); 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "types": ["vite/client", "node"], 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "jsx": "react", 17 | "allowJs": false, 18 | "isolatedModules": true, 19 | "skipLibCheck": false, 20 | "allowSyntheticDefaultImports": true, 21 | "forceConsistentCasingInFileNames": true 22 | }, 23 | "include": ["src/shell"] 24 | } 25 | -------------------------------------------------------------------------------- /src/configs/critical-libs.ts: -------------------------------------------------------------------------------- 1 | import { ENVIRONMENT, isLocal, isProduction, SERVER_BUILD_NUMBER } from "./env"; 2 | 3 | const shellBundleUrl = 4 | isLocal || SERVER_BUILD_NUMBER === 'local' ? `http://localhost:3001/js/bundle.js` : `https://1fe-a.akamaihd.net/${ENVIRONMENT}/shell/${SERVER_BUILD_NUMBER}/js/bundle.js`; 5 | 6 | const importMapOverrideUrl = isProduction ? `https://1fe-a.akamaihd.net/${ENVIRONMENT}/libs/@1fe/import-map-overrides/3.1.1/dist/import-map-overrides-api.js` : `https://1fe-a.akamaihd.net/${ENVIRONMENT}/libs/@1fe/import-map-overrides/3.1.1/dist/import-map-overrides.js`; 7 | 8 | export const criticalLibUrls = { 9 | importMapOverride: importMapOverrideUrl, 10 | systemJS: `https://1fe-a.akamaihd.net/${ENVIRONMENT}/libs/systemjs/6.14.0/dist/system.min.js`, 11 | systemJSAmd: `https://1fe-a.akamaihd.net/${ENVIRONMENT}/libs/systemjs/6.14.0/dist/extras/amd.min.js`, 12 | shellBundleUrl, 13 | }; -------------------------------------------------------------------------------- /src/__tests__/tests/shell/preload/fetch-wsk.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const runtimeOverride = { 4 | '@1fe/widget-starter-kit': { 5 | preload: [ 6 | { 7 | apiGet: '/version', 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | const wskRuntimeOverrideUrl = `http://localhost:3001/widget-starter-kit?runtime_config_overrides=${JSON.stringify(runtimeOverride)}`; 14 | 15 | test("Version API Call is preloaded due to WSK's runtimeConfig declaration @preloadAPI @e2e", async ({ 16 | page, 17 | }) => { 18 | await page.goto(wskRuntimeOverrideUrl); 19 | 20 | // confirm a link rel="preload" exists with the as attribute "fetch" and the href attribute "/version" with the crossorigin attribute "anonymous" 21 | const preloadLink = await page.$( 22 | 'link[rel="preload"][as="fetch"][href="/version"][crossorigin="anonymous"]', 23 | ); 24 | expect(preloadLink).toBeTruthy(); 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/experience.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('utils.experience.title.set', async ({ page }) => { 4 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 5 | 6 | let title = await page.title(); 7 | 8 | expect(title).toBe('1FE Starter App'); 9 | 10 | // click button to set title and the title should change to "hello world" 11 | await page.click('button[data-qa="utils.experience.title.set"]'); 12 | 13 | title = await page.title(); 14 | 15 | const expectedTitle = /hello world/i; 16 | expect(title).toMatch(expectedTitle); 17 | }); 18 | 19 | test('utils.experience.title.get', async ({ page }) => { 20 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 21 | 22 | page.on('dialog', async (dialog) => { 23 | expect(dialog.message()).toBe('1FE Starter App'); 24 | await dialog.dismiss(); 25 | }); 26 | 27 | await page.click('button[data-qa="utils.experience.title.get"]'); 28 | }); 29 | -------------------------------------------------------------------------------- /bundle/client/sw.js: -------------------------------------------------------------------------------- 1 | const { shouldUseDevelopmentMode } = require('./utils'); 2 | const { resolve } = require('path'); 3 | const tsConfigSW = resolve(__dirname, '../../tsconfig-sw.json'); 4 | 5 | const swConfig = /** @type {import('webpack').Configuration} */ { 6 | mode: shouldUseDevelopmentMode ? 'development' : 'production', 7 | devtool: shouldUseDevelopmentMode ? 'source-map' : false, 8 | entry: { 9 | sw: resolve(__dirname, '../../src/sw.ts'), 10 | }, 11 | output: { 12 | path: resolve(__dirname, '../../dist/public'), 13 | filename: '[name].js', 14 | publicPath: '', 15 | libraryTarget: 'umd', 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.tsx?$/, 24 | use: { 25 | loader: 'ts-loader', 26 | options: { 27 | configFile: tsConfigSW, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | }; 34 | 35 | module.exports = swConfig; 36 | -------------------------------------------------------------------------------- /src/csp-configs.ts: -------------------------------------------------------------------------------- 1 | import { CSPPerEnvironment } from '@1fe/server'; 2 | 3 | const commonCsp = { 4 | scriptSrc: ["'self'", 'https://1fe-a.akamaihd.net'], 5 | styleSrc: ["'unsafe-inline'"], 6 | connectSrc: ["'self'", 'https://1fe-a.akamaihd.net'], 7 | }; 8 | 9 | export const enforcedDefaultCsp: Record = { 10 | development: { 11 | ...commonCsp, 12 | scriptSrc: [...commonCsp.scriptSrc, "'unsafe-eval'"], // required for the playground to pass widget props 13 | }, 14 | integration: { 15 | ...commonCsp, 16 | scriptSrc: [...commonCsp.scriptSrc, "'unsafe-eval'", "http://127.0.0.1:*", "http://localhost:*"], // required for the playground to pass widget props 17 | connectSrc: [...commonCsp.connectSrc, "http://127.0.0.1:*", "http://localhost:*"], 18 | }, 19 | production: commonCsp, 20 | }; 21 | 22 | export const reportOnlyDefaultCsp: Record = { 23 | development: { 24 | ...commonCsp, 25 | frameAncestors: ['test-domain.com'], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/localStorage.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('Displays a string value that is set in localStorage', async ({ 4 | page, 5 | }) => { 6 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 7 | 8 | await page.click( 9 | 'button[data-qa="utils.localAndSessionStorage1FE.localStorage.get.btn"]', 10 | ); 11 | 12 | const resultElement = page.locator( 13 | 'pre[data-qa="utils.localStorage.get.result"]', 14 | ); 15 | 16 | await expect(resultElement).toHaveText('value1'); 17 | }); 18 | 19 | test('Displays a boolean value that is set in localStorage', async ({ 20 | page, 21 | }) => { 22 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 23 | await page.click( 24 | 'button[data-qa="utils.localAndSessionStorage1FE.localStorage.getBoolean.btn"]', 25 | ); 26 | 27 | const resultElement = page.locator( 28 | 'pre[data-qa="utils.localStorage.get.result"]', 29 | ); 30 | 31 | await expect(resultElement).toHaveText('true'); 32 | }); 33 | -------------------------------------------------------------------------------- /bundle/client/shell-types.js: -------------------------------------------------------------------------------- 1 | const { shouldUseDevelopmentMode } = require('./utils'); 2 | const { resolve } = require('path'); 3 | const shellTypesTsConfig = resolve( 4 | __dirname, 5 | '../../tsconfig-shell.types.json', 6 | ); 7 | 8 | const shellTypesConfig = /** @type {import('webpack').Configuration} */ { 9 | mode: shouldUseDevelopmentMode ? 'development' : 'production', 10 | devtool: shouldUseDevelopmentMode ? 'source-map' : false, 11 | entry: [resolve(__dirname, '../../src/shell/index.ts')], 12 | output: { 13 | path: resolve(__dirname, '../../dist/shell'), 14 | filename: '[name].js', 15 | publicPath: '', 16 | libraryTarget: 'umd', 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: { 26 | loader: 'ts-loader', 27 | options: { 28 | configFile: shellTypesTsConfig, 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | }; 35 | 36 | module.exports = shellTypesConfig; 37 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/sessionStorage.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('Displays a string value that is set in sessionStorage', async ({ 4 | page, 5 | }) => { 6 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 7 | 8 | await page.click( 9 | 'button[data-qa="utils.localAndSessionStorage1FE.sessionStorage.getSessionString.btn"]', 10 | ); 11 | 12 | const resultElement = page.locator( 13 | 'pre[data-qa="utils.sessionStorage.getSession.result"]', 14 | ); 15 | 16 | await expect(resultElement).toHaveText('sessionStringValue'); 17 | }); 18 | 19 | test('Displays a boolean value that is set in sessionStorage', async ({ 20 | page, 21 | }) => { 22 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 23 | await page.click( 24 | 'button[data-qa="utils.localAndSessionStorage1FE.sessionStorage.getSessionBoolean.btn"]', 25 | ); 26 | 27 | const resultElement = page.locator( 28 | 'pre[data-qa="utils.sessionStorage.getSession.result"]', 29 | ); 30 | 31 | await expect(resultElement).toHaveText('true'); 32 | }); 33 | -------------------------------------------------------------------------------- /bundle/server/dev.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const { isUndefined } = require('lodash'); 3 | const { default: start } = require('@es-exec/esbuild-plugin-start'); 4 | 5 | const { commonEsbuild } = require('./common'); 6 | const path = require('path'); 7 | 8 | const DEV_ENVIRONMENT_VARIABLES = { 9 | // overridable 10 | IS_TEST_RUN: isUndefined(process.env.IS_TEST_RUN) 11 | ? '0' 12 | : process.env.IS_TEST_RUN, 13 | ...process.env, 14 | // non-overridable 15 | NODE_ENV: 'development', 16 | IS_DEVELOPMENT: '1', 17 | DISABLE_LOGGING: '1', 18 | }; 19 | 20 | (async () => { 21 | const ctx = await esbuild.context({ 22 | ...commonEsbuild, 23 | plugins: [ 24 | ...(commonEsbuild.plugins || []), 25 | start({ 26 | env: DEV_ENVIRONMENT_VARIABLES, 27 | script: 'node dist/server.js', 28 | }), 29 | ], 30 | }); 31 | 32 | await ctx.watch(); 33 | await ctx.serve({ 34 | servedir: path.resolve(__dirname, '../../dist'), 35 | port: 3002, // need to host the server bundle on a different port than we host the application (GH actions complains) 36 | }); 37 | })(); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Docusign Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bundle/server/common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const { copy } = require('esbuild-plugin-copy'); 3 | // const browserslist = require('browserslist'); 4 | 5 | // const DEFUNCT_BROWSERS_LIST_QUERY = 6 | // '<100%, not supports es6-module, dead, edge<19'; 7 | 8 | // Your CSS/JS build tool code 9 | // const BROWSERS_LIST = browserslist(DEFUNCT_BROWSERS_LIST_QUERY); 10 | 11 | const commonEsbuild = { 12 | entryPoints: [path.resolve(__dirname, '../../src/server.ts')], 13 | bundle: true, 14 | platform: 'node', 15 | minify: false, 16 | sourcemap: 'inline', 17 | target: ['ESNext'], 18 | // packages: 'external', 19 | // mainFields: ['module', 'main'], 20 | outfile: path.resolve(__dirname, '../../dist/server.js'), 21 | // define: { 22 | // BROWSERS_LIST_CONFIG: JSON.stringify(BROWSERS_LIST), 23 | // }, 24 | // plugins: [ 25 | // copy({ 26 | // // this is equal to process.cwd(), which means we use cwd path as base path to resolve `to` path 27 | // // if not specified, this plugin uses ESBuild.build outdir/outfile options as base path. 28 | // resolveFrom: 'cwd', 29 | // assets: { 30 | // from: ['./src/static/**/*'], 31 | // to: ['./dist/public/'], 32 | // }, 33 | // }), 34 | // ], 35 | }; 36 | 37 | module.exports = { 38 | commonEsbuild, 39 | }; 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | inputs: 12 | node_version_override: 13 | description: 'Override Node.js version for this run (e.g., 20)' 14 | required: false 15 | type: string 16 | 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | packages: write 21 | id-token: write 22 | 23 | jobs: 24 | run_ci: 25 | name: CI/CD 26 | uses: docusign/1fe-ci-cd/.github/workflows/ci-starter-app.yml@main 27 | permissions: 28 | contents: write 29 | pull-requests: write 30 | packages: write 31 | id-token: write 32 | with: 33 | node-version: ${{ github.event.inputs.node_version_override || '22' }} 34 | secrets: 35 | SSH_PRIVATE_1FE: ${{ secrets.SSH_PRIVATE_1FE }} 36 | AKAMAI_NS_SSH_PRIVATE_KEY: ${{ secrets.AKAMAI_NS_SSH_PRIVATE_KEY }} 37 | AZUREAPPSERVICE_CLIENTID: ${{ secrets.AZUREAPPSERVICE_CLIENTID }} 38 | AZUREAPPSERVICE_TENANTID: ${{ secrets.AZUREAPPSERVICE_TENANTID }} 39 | AZUREAPPSERVICE_SUBSCRIPTIONID: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID }} 40 | AZURE_RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /src/server/exceptions/APIException.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from './HttpException'; 2 | import { ErrorCodes } from './errorCodes'; 3 | 4 | /** 5 | * Exception on an API endpoint where we want to return json 6 | */ 7 | export class APIException extends HttpException { 8 | public code: ErrorCodes; 9 | public internalMessage: string; 10 | 11 | /** 12 | * Construct a new api exception 13 | * @param httpStatus The http status to return on the response 14 | * @param code The internal error code 15 | * @param message Error message to help the end user 16 | * @param internalMessage Internal error message to help the developer. Not sent to client. 17 | * @param cause Original error that caused this exception 18 | */ 19 | constructor({ 20 | status, 21 | code, 22 | message, 23 | internalMessage, 24 | cause, 25 | }: { 26 | status: number; 27 | code: ErrorCodes; 28 | message: string; 29 | internalMessage: string; 30 | cause?: Error; 31 | }) { 32 | super(status, message); 33 | this.code = code; 34 | this.cause = cause; 35 | this.internalMessage = internalMessage; 36 | } 37 | 38 | toString(): string { 39 | const result = `${super.toString()}: code [${this.code}] internalMessage [${ 40 | this.internalMessage 41 | }]`; 42 | 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/__tests__/test-utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { Page, expect } from '@playwright/test'; 2 | 3 | export const getLocalStorage = async (page: Page) => { 4 | const localStorage = (await page.evaluate(() => { 5 | return Object.entries(window.localStorage).map(([key, value]) => ({ 6 | name: key, 7 | value, 8 | })); 9 | })) as Record[]; 10 | 11 | return localStorage; 12 | }; 13 | 14 | // This older version of generic child has a simpler runtime config, used to test runtime config overrides 15 | export const expectedLocalStorageForGenericChildOneOhTwenty = 16 | expect.arrayContaining([ 17 | { 18 | name: `@1ds/shell.runtime_config_@internal/generic-child-widget`, 19 | value: '"{\\"preload\\":[{\\"apiGet\\":\\"/version\\"}]}"', 20 | }, 21 | { 22 | name: 'import-map-override:@internal/generic-child-widget', 23 | value: 24 | 'https://docutest-a.akamaihd.net/integration/1ds/widgets/@internal/generic-child-widget/1.0.20/js/1ds-bundle.js', 25 | }, 26 | ]); 27 | 28 | export const expectedCurrentGenericChildRuntimeConfig = { 29 | preload: expect.any(Array), 30 | dependsOn: { 31 | pinnedWidgets: [ 32 | { 33 | version: expect.any(String), 34 | widgetId: '@internal/generic-pinned-widget', 35 | }, 36 | ], 37 | }, 38 | }; 39 | 40 | export const expectedOldGenericChildRuntimeConfig = { 41 | preload: [{ apiGet: '/version' }], 42 | }; 43 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/browser-not-supported.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page } from '@playwright/test'; 2 | 3 | const checkForVisibleUnsupportedBrowserPage = async ( 4 | page: Page, 5 | isSupported = false, 6 | ) => { 7 | const url = 'http://localhost:3001/widget-starter-kit/utils'; 8 | await page.goto(url); 9 | 10 | const pageTitle = await page.title(); 11 | 12 | if (isSupported) { 13 | expect(pageTitle).not.toContain('Unsupported Browser'); 14 | } else { 15 | expect(pageTitle).toContain('Unsupported Browser'); 16 | } 17 | }; 18 | 19 | test.describe.skip('test outdated user agent (chrome)', () => { 20 | test.use({ 21 | userAgent: 22 | 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36', 23 | }); 24 | 25 | test('should show "unsupported browser" page', async ({ page }) => { 26 | await checkForVisibleUnsupportedBrowserPage(page); 27 | }); 28 | }); 29 | 30 | test.describe('future latest agent (chrome)', () => { 31 | test.use({ 32 | userAgent: 33 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', 34 | }); 35 | 36 | // TODO @sushruth - Not sure why but this test is broken for now. 37 | test.skip('should NOT show "unsupported browser" page @e2e', async ({ 38 | page, 39 | }) => { 40 | await checkForVisibleUnsupportedBrowserPage(page, true); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/page-not-found.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('the app redirects to HTTP_404_NOT_FOUND_URL on unmatched route', async ({ 4 | page, 5 | }) => { 6 | await page.goto('http://localhost:3001/not-valid'); 7 | 8 | const notFoundHeader = page.locator( 9 | 'h1[data-qa="shell.notFound.page.header"]', 10 | ); 11 | await expect(notFoundHeader).toBeVisible(); 12 | 13 | expect(page.url()).toBe('http://localhost:3001/not-valid'); 14 | }); 15 | 16 | test('the app redirects to HTTP_404_NOT_FOUND_URL on unmatched route with valid query params', async ({ 17 | page, 18 | }) => { 19 | await page.goto('http://localhost:3001/not-valid?test=test'); 20 | 21 | const notFoundHeader = page.locator( 22 | 'h1[data-qa="shell.notFound.page.header"]', 23 | ); 24 | await expect(notFoundHeader).toBeVisible(); 25 | 26 | expect(page.url()).toBe('http://localhost:3001/not-valid?test=test'); 27 | }); 28 | 29 | test('If HTTP_404_NOT_FOUND_URL is not send, can use the go back button to go back a page', async ({ 30 | page, 31 | }) => { 32 | await page.goto('http://localhost:3001/widget-starter-kit'); 33 | await page.goto('http://localhost:3001/not-valid'); 34 | 35 | const notFoundHeader = page.locator( 36 | 'h1[data-qa="shell.notFound.page.header"]', 37 | ); 38 | await expect(notFoundHeader).toBeVisible(); 39 | 40 | await page.goBack(); 41 | 42 | await expect(page.url()).toBe('http://localhost:3001/widget-starter-kit'); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/csp/csp-runtime-override.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const runtimeOverride = { 4 | '@1fe/widget-starter-kit': { 5 | headers: { 6 | csp: { 7 | enforced: { 8 | imgSrc: ['https://overriden-test.com'], 9 | }, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | const wskRuntimeOverrideUrl = `http://localhost:3001/widget-starter-kit?runtime_config_overrides=${JSON.stringify(runtimeOverride)}`; 16 | 17 | test('should override csp only on integration', async ({ page }) => { 18 | // Listen to any CSP violation errors in console 19 | const consoleErrors: string[] = []; 20 | page.on('console', (msg) => { 21 | if (msg.type() === 'error') { 22 | consoleErrors.push(msg.text()); 23 | } 24 | }); 25 | 26 | const pageResponse = page.waitForResponse( 27 | (response) => 28 | response.url().includes('http://localhost:3001/widget-starter-kit') && 29 | response.status() === 200, 30 | ); 31 | 32 | await page.goto(wskRuntimeOverrideUrl); 33 | await page.waitForLoadState('load'); 34 | 35 | // Grab csp from page response 36 | const headers = (await pageResponse).headers(); 37 | const cspHeader = headers['content-security-policy']; 38 | 39 | expect(cspHeader).toContain('https://overriden-test.com'); 40 | 41 | // should have no csp violations in the console. 42 | const cspViolations = consoleErrors.filter((error: string) => 43 | error.includes('Content Security Policy'), 44 | ); 45 | expect(cspViolations).toEqual([]); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/test-utils/importMapOverridesUi.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | 3 | export const selectImportMapOverrideButton = (page: Page) => 4 | page.locator('button.imo-trigger'); 5 | 6 | export const selectDevtoolImportMapOverrideButton = (page: Page) => 7 | page.getByTestId('1ds-devtool-import-map-ui-button'); 8 | 9 | export const overrideWidgetUrlWithUi = async ({ 10 | page, 11 | widgetId, 12 | widgetBundleUrl, 13 | }: { 14 | page: Page; 15 | widgetId: string; 16 | widgetBundleUrl: string; 17 | }) => { 18 | // if (FEATURE_FLAGS.enable1dsDevtool && isIntegrationEnvironment(ENVIRONMENT)) { 19 | // await selectDevtoolImportMapOverrideButton(page).click(); 20 | // } else { 21 | await selectImportMapOverrideButton(page).click(); 22 | // } 23 | 24 | await page.getByPlaceholder('Search modules').fill('generic-child-widget'); 25 | await page.getByRole('button', { name: `Default ${widgetId}` }).click(); 26 | await page.getByLabel('Override URL:').click(); 27 | await page.getByLabel('Override URL:').fill(widgetBundleUrl); 28 | await page.getByRole('button', { name: 'Apply override' }).click(); 29 | 30 | await page.waitForTimeout(1000); 31 | }; 32 | 33 | export const resetAllOverrides = async (page: Page) => { 34 | const resetButton = page.getByRole('button', { name: 'Reset all overrides' }); 35 | 36 | if (await resetButton.isVisible()) { 37 | await resetButton.click(); 38 | } else { 39 | await selectImportMapOverrideButton(page).dispatchEvent('click'); 40 | const resetButton = page.getByRole('button', { 41 | name: 'Reset all overrides', 42 | }); 43 | await resetButton.click(); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/overrides/meta-tags.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const metaTags = [ 4 | { 5 | name: 'title', 6 | content: 'Hello world SEO!', 7 | }, 8 | { 9 | name: 'description', 10 | content: 'Smart applications for all', 11 | }, 12 | { 13 | name: 'keywords', 14 | content: 'docusign, tutorial, seo, tags', 15 | }, 16 | { 17 | name: 'robots', 18 | content: 'noindex, nofollow', 19 | }, 20 | { 21 | name: 'viewport', 22 | content: 'width=device-width, initial-scale=1.0', 23 | }, 24 | { 25 | name: 'revisit-after', 26 | content: '5 days', 27 | }, 28 | { 29 | name: 'language', 30 | content: 'en-US', 31 | }, 32 | { 33 | 'http-equiv': 'content-type', 34 | content: 'text/html; charset=utf-8', 35 | }, 36 | ]; 37 | 38 | const runtimeOverride = { 39 | '@1fe/widget-starter-kit': { 40 | plugin: { 41 | metaTags, 42 | }, 43 | }, 44 | }; 45 | 46 | const wskRuntimeOverrideUrl = `http://localhost:3001/widget-starter-kit?runtime_config_overrides=${JSON.stringify(runtimeOverride)}`; 47 | 48 | test("Ensure metaTags from a widget's runtimeConfigs are added to the HTML response", async ({ 49 | page, 50 | }) => { 51 | await page.goto(wskRuntimeOverrideUrl); 52 | 53 | // ensure the following meta tags are present in the HTML response 54 | 55 | for (const tag of metaTags) { 56 | const tagAttributes = Object.entries(tag).reduce( 57 | (itr, e) => `${itr}[${e[0]}="${e[1]}"]`, 58 | '', 59 | ); 60 | 61 | // expect to exist in the DOM 62 | expect(await page.$(`meta${tagAttributes}`)).not.toBeUndefined(); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/preload/api-get.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const runtimeOverride = { 4 | '@1fe/starter-kit': { 5 | preload: [ 6 | { 7 | apiGet: 8 | 'https://docutest-a.akamaihd.net/development/1fe/widgets/@1fe/starter-kit/1.0.0/widget-runtime-config.json', 9 | }, 10 | ], 11 | }, 12 | }; 13 | 14 | const wskRuntimeOverrideUrl = `http://localhost:3001/widget-starter-kit?runtime_config_overrides=${JSON.stringify(runtimeOverride)}`; 15 | 16 | // TODO[1fe][post-mvp]: Use templatized w/ more complex examples 17 | // TODO @sushruth - this test is broken and needs to be fixed. 18 | test.skip('Ensure widgets with templatized apiGet has correct request url', async ({ 19 | page, 20 | }) => { 21 | await page.goto(wskRuntimeOverrideUrl); 22 | 23 | const versionData = await fetch('http://localhost:3001/version').then((e) => 24 | e.json(), 25 | ); 26 | const { 27 | configs: { widgetConfig }, 28 | } = versionData; 29 | // const widgetId = '@1fe/starter-kit'; 30 | console.log(widgetConfig); 31 | // const version = widgetConfig.find( 32 | // (plugin: any) => plugin.widgetId === widgetId, 33 | // )?.version; 34 | // const hostedEnv = process.env.NODE_ENV?.toLowerCase(); 35 | 36 | // sanity check since NODE_ENV could be undefined 37 | // if (hostedEnv) { 38 | // const cdnBaseUrl = 'https://docutest-a.akamaihd.net'; 39 | const expectedRequestUrl = 40 | 'https://1fe-a.akamaihd.net/integration/widgets/@1fe/widget-starter-kit/1.0.17/widget-runtime-config.json'; 41 | const requestPromise = page.waitForRequest(expectedRequestUrl); 42 | await page.reload(); 43 | const request = await requestPromise; 44 | expect(request).toBeTruthy(); 45 | // } 46 | }); 47 | -------------------------------------------------------------------------------- /src/shell/main.tsx: -------------------------------------------------------------------------------- 1 | import renderOneFEShell, { OneFEErrorComponentProps } from '@1fe/shell'; 2 | import React from 'react'; 3 | 4 | import { Loader } from './components/Loader'; 5 | import { Error } from './components/Error'; 6 | import { shellLogger } from './logger'; 7 | import { CustomExampleUtils } from './types/utils'; 8 | import { initServiceWorker } from './utils/init-service-worker'; 9 | 10 | const exampleCustomUtils: CustomExampleUtils = { 11 | initializeLogger: (widgetId: string) => ({ 12 | logger: { 13 | log: (message: string) => { 14 | console.log(`[${widgetId}]`, message); 15 | }, 16 | error: (message: string) => { 17 | console.error(`[${widgetId}]`, message); 18 | }, 19 | }, 20 | }), 21 | }; 22 | 23 | const setup = () => { 24 | renderOneFEShell({ 25 | hooks: { 26 | onBeforeRenderShell: () => { 27 | // This is a good place to initialize global state, register service workers, etc. 28 | initServiceWorker(); 29 | }, 30 | }, 31 | utils: exampleCustomUtils, 32 | auth: { 33 | isAuthedCallback: (widgetId: string): boolean => { 34 | console.log(widgetId, ' is authenticated.'); 35 | return false; 36 | }, 37 | unauthedCallback: (widgetId: string) => { 38 | console.log(widgetId, ' is not authenticated.'); 39 | }, 40 | }, 41 | shellLogger: { 42 | ...shellLogger, 43 | logPlatformUtilUsage: true, 44 | redactSensitiveData: true, 45 | }, 46 | components: { 47 | getLoader: () => , 48 | getError: (props?: OneFEErrorComponentProps) => , 49 | }, 50 | routes: { 51 | defaultRoute: '/widget-starter-kit', 52 | }, 53 | }); 54 | }; 55 | 56 | setup(); 57 | -------------------------------------------------------------------------------- /src/server/middlewares/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import ejs from 'ejs'; 3 | 4 | import { HttpException } from '../exceptions/HttpException'; 5 | import { APIException } from '../exceptions/APIException'; 6 | import { APIErrorResponse } from '../types/APIErrorResponse'; 7 | import { errorTemplate } from './error-template'; 8 | 9 | type RequestWithCspNonceGuid = Request & { cspNonceGuid?: string }; 10 | 11 | const errorMiddleware = ( 12 | error: HttpException | APIException, 13 | req: RequestWithCspNonceGuid, 14 | res: Response, 15 | next: NextFunction, 16 | ): void => { 17 | try { 18 | const status: number = error.status || 500; 19 | const message: string = error.message || 'Something went wrong'; 20 | 21 | // Console logging 22 | console.error( 23 | `[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`, 24 | ); 25 | 26 | // if an error has been thrown from an API route then serve a json response 27 | if (error instanceof APIException) { 28 | const apiError: APIErrorResponse = { 29 | errorCode: error.code, 30 | errorMessage: error.message, 31 | }; 32 | res.status(error.status).json(apiError); 33 | return; 34 | } 35 | 36 | // return the standard error page 37 | const { cspNonceGuid } = req; 38 | 39 | const pageTitle = error.message || 'Error'; 40 | 41 | const dataForRenderingTemplatePayload = { 42 | cspNonceGuid, 43 | type: 'error', 44 | pageTitle, 45 | favicon: '/favicon.ico', 46 | }; 47 | 48 | const html = ejs.render(errorTemplate, dataForRenderingTemplatePayload); 49 | res.send(html); 50 | } catch (err) { 51 | next(err); 52 | } 53 | }; 54 | 55 | export default errorMiddleware; 56 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import express from 'express'; 3 | import path from 'path'; 4 | import oneFEServer from '@1fe/server'; 5 | import favicon from 'serve-favicon'; 6 | 7 | import router from './lib/router'; 8 | import { enforcedDefaultCsp, reportOnlyDefaultCsp } from './csp-configs'; 9 | import errorMiddleware from './server/middlewares/error.middleware'; 10 | import { ENVIRONMENT, ExampleHostedEnvironments, isLocal, isProduction } from './configs/env'; 11 | import { criticalLibUrls } from './configs/critical-libs'; 12 | import { configManagement } from './configs/ecosystem-configs'; 13 | 14 | dotenv.config(); 15 | 16 | const { PORT = 3001 } = process.env; 17 | 18 | const options = { 19 | environment: isLocal ? ExampleHostedEnvironments.integration : ENVIRONMENT, 20 | isProduction, 21 | orgName: '1FE Starter App', 22 | configManagement, 23 | criticalLibUrls, 24 | csp: { 25 | defaultCSP: { 26 | enforced: enforcedDefaultCsp[ENVIRONMENT], 27 | reportOnly: reportOnlyDefaultCsp[ENVIRONMENT], 28 | } 29 | }, 30 | }; 31 | 32 | async function startServer() { 33 | const app = await oneFEServer(options); 34 | 35 | // Middleware that parses json and looks at requests where the Content-Type header matches the type option. 36 | app.use(express.json()); 37 | 38 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 39 | // @ts-ignore 40 | app.use(favicon(path.join(__dirname, 'public/favicon.ico'))); 41 | 42 | // Serve API requests from the router 43 | app.use('/api', router); 44 | 45 | app.use(errorMiddleware); 46 | 47 | // Set EJS as the view engine 48 | app.set('view engine', 'ejs'); 49 | 50 | app.listen(PORT, () => { 51 | console.log(`Server listening at http://localhost:${PORT}`); 52 | }); 53 | } 54 | 55 | startServer(); 56 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/overrides/sanitize-query-params.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | import { setUserAgentForWidgetUrlOverride } from '../../../test-utils/setUserAgentForWidgetUrlOverride'; 4 | 5 | // const PREPROD_ENVS = ['integration', 'stage', 'integration:1ds', 'stage:1ds']; 6 | 7 | test.describe('widget_url_overrides sanitization based on user agent value', () => { 8 | const url = `http://localhost:3001/widget-starter-kit?widget_url_overrides={"shouldExist":true}&testQuery=shouldExist`; 9 | 10 | test('widget_url_overrides should NOT be removed if user agent doesnt include allowed user agent values @e2e', async ({ 11 | page, 12 | }) => { 13 | await setUserAgentForWidgetUrlOverride(page); 14 | await page.goto(url); 15 | 16 | const sanitizedUrl = page.url(); 17 | const urlObject = new URL(sanitizedUrl); 18 | const queryParams = new URLSearchParams(urlObject.search); 19 | 20 | expect(queryParams.has('widget_url_overrides')).toBe(true); 21 | }); 22 | 23 | test('widget_url_overrides should reflect correct value depending on environment and user agent @e2e', async ({ 24 | page, 25 | }) => { 26 | await page.goto(url); 27 | 28 | const sanitizedUrl = page.url(); 29 | const urlObject = new URL(sanitizedUrl); 30 | const queryParams = new URLSearchParams(urlObject.search); 31 | // const isPreprod = PREPROD_ENVS.includes( 32 | // process.env.NODE_ENV?.toLowerCase() || '', 33 | // ); 34 | 35 | // if (isPreprod) { 36 | // if preprod, widget_url_overrides and testQuery should be true 37 | expect(queryParams.has('widget_url_overrides')).toBe(true); 38 | expect(queryParams.has('testQuery')).toBe(true); 39 | // } else { 40 | // // if prod, widget_url_overrides should be removed, but testQuery should still exist 41 | // expect(queryParams.has('widget_url_overrides')).toBe(false); 42 | // expect(queryParams.has('testQuery')).toBe(true); 43 | // } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/preload/shell-and-plugin.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Uncomment this test when we have cdn hosting figured out. 2 | // import { test, expect } from '@playwright/test'; 3 | 4 | // test( 5 | // 'Shell and Plugin bundles are preloaded & loaded in HTML @preloadAssets @e2e', 6 | // async ({ page }) => { 7 | // // store all network events in an array 8 | // const networkEvents: any = []; 9 | // page.on('request', (request) => { 10 | // networkEvents.push(request.url()); 11 | // }); 12 | // await page.goto('http://localhost:3001/widget-starter-kit'); 13 | 14 | // await page.waitForURL('http://localhost:3001/widget-starter-kit', { 15 | // waitUntil: 'load', 16 | // }); 17 | 18 | // // confirm a link rel="preload" exists with the as attribute "script" and the href attribute shellBundleUrl with the crossorigin attribute "anonymous" 19 | // const shellBundlePreloadLink = await page.$( 20 | // `link[rel="preload"][as="script"][href="${shellBundleUrl}"][crossorigin="anonymous"]`, 21 | // ); 22 | // expect(shellBundlePreloadLink).toBeTruthy(); 23 | 24 | // // confirm a link rel="preload" exists with the as attribute "script" and the href attribute pluginBundleUrl with the crossorigin attribute "anonymous" 25 | // const pluginBundlePreloadLink = await page.$( 26 | // `link[rel="preload"][as="script"][href="${pluginBundleUrl}"][crossorigin="anonymous"]`, 27 | // ); 28 | // expect(pluginBundlePreloadLink).toBeTruthy(); 29 | 30 | // // confirm shellBundlePreloadLink and pluginBundlePreloadLink are present in the network request activity 31 | // // getEntriesByType('resource') returns an array of all network requests that does not include preload information 32 | // const networkRequests = await getNetworkResourceEntriesFromPage(page); 33 | // expect(networkRequests.includes(shellBundleUrl)).toBeTruthy(); 34 | // expect(networkRequests.includes(pluginBundleUrl)).toBeTruthy(); 35 | // }, 36 | // ); 37 | -------------------------------------------------------------------------------- /src/server/middlewares/error-template.ts: -------------------------------------------------------------------------------- 1 | export const errorTemplate = ` 2 | 3 | 4 | 5 | 6 | 7 | <%- pageTitle %> 8 | 9 | 10 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | <% if(type === 'unsupported' ){ %> 35 |

Unsupported Browser

36 |

37 | You’re using a version that isn’t supported, update to 38 | the latest version to continue. 39 |

40 | <% } else { %> 41 |

An error has occurred

42 |

Make sure your connection is stable and try again

43 | 46 | <% } %> 47 |
48 |
49 |
50 | 51 | 67 | 68 | 69 | `; 70 | -------------------------------------------------------------------------------- /bundle/client/dev.js: -------------------------------------------------------------------------------- 1 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 2 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 3 | const { EnvironmentPlugin, HotModuleReplacementPlugin } = require('webpack'); 4 | 5 | const { isUndefined } = require('lodash'); 6 | const getProdConfig = require('./prod'); 7 | const { commonPlugins } = require('./utils'); 8 | 9 | /** 10 | * This config is used for local development of the 1FE shell. 11 | * Remote development mode builds are handled internally by getProdConfig. 12 | */ 13 | const config = getProdConfig({ 14 | mode: 'development', 15 | devtool: 'source-map', 16 | devServer: { 17 | hot: true, 18 | open: !process.env.IS_TEST_RUN, 19 | client: { overlay: { errors: true, warnings: false }, logging: 'info' }, 20 | historyApiFallback: true, // fixes error 404-ish errors when using react router :see this SO question: https://stackoverflow.com/questions/43209666/react-router-v4-cannot-get-url 21 | allowedHosts: 'all', 22 | proxy: { 23 | '*': { 24 | target: 'http://localhost:3000', 25 | logLevel: 'debug', 26 | changeOrigin: true, 27 | secure: false, 28 | }, 29 | context: () => true, 30 | }, 31 | devMiddleware: { 32 | index: false, 33 | mimeTypes: { png: 'image/png' }, 34 | }, 35 | }, 36 | externals: { 37 | react: 'React', 38 | 'react-dom': 'ReactDOM', 39 | 'react-router': 'ReactRouter', 40 | 'react-router-dom': 'ReactRouterDOM', 41 | '@remix-run/router': 'RemixRouter', 42 | }, 43 | plugins: [ 44 | ...commonPlugins, 45 | new HotModuleReplacementPlugin(), 46 | new ReactRefreshWebpackPlugin({ 47 | overlay: false, 48 | }), 49 | new EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | IS_TEST_RUN: isUndefined(process.env.IS_TEST_RUN) 52 | ? '0' 53 | : process.env.IS_TEST_RUN, 54 | }), 55 | new ProgressBarPlugin({ 56 | total: 20, 57 | format: `build [:bar] :percent (:elapsed seconds)`, 58 | clear: true, 59 | }), 60 | ], 61 | }); 62 | 63 | module.exports = config; 64 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/appLoadTime.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('Should successfully Mark Start and Mark End for generic-child-widget', async ({ 4 | page, 5 | }) => { 6 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 7 | 8 | await page.click('button[data-qa="utils.appLoadTime.getEntries.btn"]'); 9 | 10 | const resultElement = page.locator( 11 | 'div[data-qa="utils.appLoadTime.getEntries.result"]', 12 | ); 13 | 14 | await expect(resultElement).not.toContainText('@1fe/sample-widget-start'); 15 | await expect(resultElement).not.toContainText('@1fe/sample-widget-end'); 16 | 17 | await page.click('button[data-qa="utils.appLoadTime.get.btn"]'); 18 | 19 | await page.waitForTimeout(500); 20 | 21 | await page.click('button[data-qa="utils.appLoadTime.getEntries.btn"]'); 22 | 23 | await expect(resultElement).toContainText('@1fe/sample-widget-start'); 24 | await expect(resultElement).toContainText('@1fe/sample-widget-end'); 25 | }); 26 | 27 | test('Should get all entries', async ({ page }) => { 28 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 29 | 30 | await page.click('button[data-qa="utils.appLoadTime.getEntries.btn"]'); 31 | 32 | const resultElement = page.locator( 33 | 'div[data-qa="utils.appLoadTime.getEntries.result"]', 34 | ); 35 | 36 | await expect(resultElement).toContainText('Entry Type: mark'); 37 | }); 38 | 39 | test('Should mark and measure custom events', async ({ page }) => { 40 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 41 | 42 | const resultElement = page.locator( 43 | 'div[data-qa="utils.appLoadTime.measure.result"]', 44 | ); 45 | 46 | await page.click('button[data-qa="utils.appLoadTime.mark.btn"]'); 47 | 48 | await expect(resultElement).toHaveText('Mark started'); 49 | 50 | await page.click('button[data-qa="utils.appLoadTime.measure.btn"]'); 51 | 52 | await expect(resultElement).toContainText( 53 | '@1fe/widget-starter-kit-iLove1FESoMuchMarkTest', 54 | ); 55 | 56 | expect( 57 | /\d+(\.\d+)?/.test((await resultElement.innerText()) || ''), 58 | ).toBeTruthy(); 59 | }); 60 | -------------------------------------------------------------------------------- /src/configs/widgetConfigs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file serves as an example using Azure App Configuration. 3 | * 4 | * Users should be able to adapt this implementation to use alternative configuration services such as: 5 | * - AWS AppConfig 6 | * - Google Cloud Secret Manager 7 | * - Google Cloud Storage or Firestore 8 | * - Firebase Remote Config 9 | * - Or any other configuration management service of your choice 10 | * 11 | * The core functionality of fetching widget versions can be implemented 12 | * using similar patterns regardless of the underlying configuration provider. 13 | */ 14 | 15 | import { load } from '@azure/app-configuration-provider'; 16 | import { OneFEConfigManagement } from '@1fe/server'; 17 | import { ENVIRONMENT } from './env'; 18 | const connectionString = process.env.AZURE_APPCONFIG_CONNECTION_STRING; 19 | 20 | // TODO @sushruth - consider exporting this type directly from @1fe/server 21 | type WidgetVersions = OneFEConfigManagement['widgetVersions'] extends 22 | | { 23 | get: () => Promise; 24 | } 25 | | { url: string } 26 | ? T 27 | : never; 28 | 29 | export async function getWidgetVersions(): Promise { 30 | if (!connectionString) { 31 | console.log( 32 | 'AZURE_APPCONFIG_CONNECTION_STRING is not set. using a static widget version list.', 33 | ); 34 | // This is for local development or testing purposes. 35 | // In production, you should set the AZURE_APPCONFIG_CONNECTION_STRING environment variable 36 | // to connect to your Azure App Configuration instance. 37 | // The following is a mock implementation to simulate the expected output. 38 | return Promise.resolve([ 39 | { 40 | widgetId: '@1fe/playground', 41 | version: '0.1.1', 42 | }, 43 | { 44 | widgetId: '@1fe/sample-widget', 45 | version: '1.0.2', 46 | }, 47 | { 48 | widgetId: '@1fe/sample-widget-with-auth', 49 | version: '1.0.4', 50 | }, 51 | { 52 | widgetId: '@1fe/widget-starter-kit', 53 | version: '1.0.18', 54 | }, 55 | ]); 56 | } 57 | 58 | const widgetVersions = await load(connectionString, { 59 | selectors: [ 60 | { 61 | keyFilter: `${ENVIRONMENT}:*`, 62 | }, 63 | ], 64 | trimKeyPrefixes: [`${ENVIRONMENT}:`], 65 | }); 66 | 67 | return Array.from(widgetVersions.values()); 68 | } 69 | -------------------------------------------------------------------------------- /src/__tests__/tests/platform-utils/eventbus.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const mockedTimestamp = 1609477200000; // Jan 1, 2021 4 | const expectedEventData = { 5 | eventInfo: { 6 | sender: { id: '@1fe/widget-starter-kit' }, 7 | timestamp: mockedTimestamp, 8 | }, 9 | data: { param1: 'Listener is working!' }, 10 | }; 11 | 12 | test('subscribe + publish + unsubscribe', async ({ page }) => { 13 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 14 | 15 | await page.click('button[data-qa="utils.eventBus.get.btn"]'); 16 | 17 | const resultElement = page.locator( 18 | 'div[data-qa="utils.widgets.eventBus.result.container"]', 19 | ); 20 | 21 | await expect(resultElement).toContainText('unchanged'); 22 | 23 | // find publish1 button and click it, should remain unchanged 24 | await page.click('button[data-qa="utils.eventBus.publish1.btn"]'); 25 | await expect(resultElement).toContainText('unchanged'); 26 | 27 | // find subscribe button and click it, result should now say subscribed 28 | await page.click('button[data-qa="eventBus.subscribe.btn"]'); 29 | await expect(resultElement).toContainText('subscribed'); 30 | 31 | page.evaluate((mockedTimestamp) => { 32 | Date.now = () => { 33 | return mockedTimestamp; 34 | }; 35 | }, mockedTimestamp); 36 | 37 | // click publish1 button again, result should be the event data 38 | await page.click('button[data-qa="utils.eventBus.publish1.btn"]'); 39 | await expect(resultElement).toContainText(JSON.stringify(expectedEventData)); 40 | 41 | // find publish2 button and click it, nothing should change 42 | await page.click('button[data-qa="utils.eventBus.publish2.btn"]'); 43 | await expect(resultElement).toContainText(JSON.stringify(expectedEventData)); 44 | 45 | // find unsubscribe button and click it, should read unsubscirbed 46 | await page.click('button[data-qa="eventBus.unsubscribe.btn"]'); 47 | await expect(resultElement).toContainText('unsubscribed'); 48 | 49 | // clicking publish1 button again should not change the result 50 | await page.click('button[data-qa="utils.eventBus.publish1.btn"]'); 51 | await expect(resultElement).toContainText('unsubscribed'); 52 | 53 | // clicking publish2 button again should not change the result 54 | await page.click('button[data-qa="utils.eventBus.publish2.btn"]'); 55 | await expect(resultElement).toContainText('unsubscribed'); 56 | }); 57 | -------------------------------------------------------------------------------- /bundle/client/utils.js: -------------------------------------------------------------------------------- 1 | const { DefinePlugin } = require('webpack'); 2 | 3 | const BUILD_BUILDID = process.env.BUILD_BUILDID; 4 | const DEBUG_BUILD = process.env.DEBUG_BUILD === 'true'; 5 | const IS_TEST_RUN = process.env.IS_TEST_RUN; 6 | const IS_AUTOMATION_RUN = process.env.IS_AUTOMATION_RUN; 7 | 8 | const showDevtoolBasedOnEnvironment = process.env.NODE_ENV === 'development' || DEBUG_BUILD; 9 | 10 | // Define ENVIRONMENT directly matching the logic in src/configs/env.ts 11 | const ENVIRONMENT = process.env.NODE_ENV === 'development' ? 'integration' : (process.env.NODE_ENV || 'production'); 12 | 13 | const commonPlugins = [ 14 | new DefinePlugin({ 15 | // We use isProductionEnvironment because we allow the devtool on stage 16 | __SHOW_DEVTOOL__: JSON.stringify(showDevtoolBasedOnEnvironment), 17 | __ENABLE_SERVICE_WORKER__: JSON.stringify( 18 | process.env.ENABLE_SERVICE_WORKER, 19 | ), 20 | }), 21 | ]; 22 | 23 | /** 24 | * Fetches the dynamic configuration for the specified environment 25 | * @returns {Promise} The dynamic configuration object 26 | */ 27 | async function fetchDynamicConfig() { 28 | try { 29 | // Construct the URL directly since we can't import the TypeScript file 30 | const url = `https://1fe-a.akamaihd.net/${ENVIRONMENT}/configs/live.json`; 31 | console.log(`Fetching dynamic config from: ${url}`); 32 | 33 | const response = await fetch(url); 34 | const data = await response.json(); 35 | return data; 36 | } catch (error) { 37 | console.error('Failed to fetch dynamic config:', error); 38 | return null; 39 | } 40 | } 41 | 42 | /** 43 | * Gets the browserslist target configuration from dynamic config 44 | * @returns {Promise} Array of browserslist targets 45 | * @throws {Error} If the required configuration is not found 46 | */ 47 | async function getBrowserslistTargets() { 48 | let dynamicConfig = await fetchDynamicConfig(); 49 | 50 | if (Array.isArray(dynamicConfig?.platform?.browserslistConfig?.buildTarget) && 51 | dynamicConfig.platform.browserslistConfig.buildTarget.length) { 52 | return dynamicConfig.platform.browserslistConfig.buildTarget; 53 | } 54 | 55 | const errorMsg = 'Required browserslist configuration not found in dynamic config.'; 56 | throw new Error(`${errorMsg}\nReceived the following dynamic config:\n${JSON.stringify(dynamicConfig, null, 2)}`); 57 | } 58 | 59 | module.exports = { 60 | commonPlugins, 61 | BUILD_BUILDID, 62 | DEBUG_BUILD, 63 | IS_TEST_RUN, 64 | IS_AUTOMATION_RUN, 65 | getBrowserslistTargets 66 | }; -------------------------------------------------------------------------------- /src/__tests__/tests/shell/sessionIdCookie.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('session ID cookie', () => { 4 | test('session ID cookie should exists (non iframe embedded widget without auth) @e2e', async ({ 5 | page, 6 | context, 7 | }) => { 8 | await page.goto('http://localhost:3001/widget-starter-kit/utils'); 9 | 10 | const cookies = await context.cookies(); 11 | const hasSessionIdCookie = cookies.some( 12 | (cookie) => 13 | cookie.name === 'session_id' && 14 | cookie.sameSite === 'None' && 15 | cookie.httpOnly === false && 16 | cookie.secure === true, 17 | ); 18 | 19 | expect(hasSessionIdCookie).toBeTruthy(); 20 | }); 21 | 22 | // TODO[1fe][post-mvp]: add back as follow up. Need to create parent frame to test 23 | // test( 24 | // 'session ID cookie should exists and be readable from inside an iframe embedded widget @e2e', 25 | // async ({ page }) => { 26 | // // TODO: move the iframe playground to a shared repo 27 | // const iframePlaygroundUrl = 28 | // 'https://github.docusignhq.com/pages/luke-hatcher/iframe/'; 29 | // await page.goto(iframePlaygroundUrl); 30 | 31 | // page.on('dialog', async (dialog) => { 32 | // expect(dialog.message()).toMatch( 33 | // // Session ID guid regex 34 | // /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, 35 | // ); 36 | // await dialog.dismiss(); 37 | // }); 38 | 39 | // // make sure iframe playground loads 40 | // await expect(page.getByTestId('iframe.text.input')).toBeVisible(); 41 | 42 | // // type the URL in the input field 43 | // await page.fill('input[data-qa="iframe.text.input"]', wskLaunchUrl); 44 | 45 | // await page.click('button[data-qa="iframe.submit.button"]'); 46 | 47 | // await page.pause(); 48 | 49 | // const iframeLocator = page.frameLocator('iframe[data-qa="iframe.frame"]'); 50 | 51 | // const isEmbeddedWskRendered = iframeLocator.getByTestId('wsk.page.root'); 52 | // await expect(isEmbeddedWskRendered).toBeVisible(); 53 | 54 | // // navigate to foo page 55 | // const fooHeaderButton = iframeLocator?.getByTestId( 56 | // 'wsk.header.button.foo', 57 | // ); 58 | // await expect(fooHeaderButton).toBeVisible(); 59 | // await fooHeaderButton.click(); 60 | // await expect(iframeLocator?.getByTestId('wsk.page.foo')).toBeVisible(); 61 | 62 | // // click sessionId button 63 | // await iframeLocator?.getByTestId('wsk.show.sessionId.btn').click(); 64 | // }, 65 | // ); 66 | }); 67 | -------------------------------------------------------------------------------- /src/__tests__/test-utils/envs.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | 3 | // import { HostedEnvironment } from '@1ds/helpers/types'; 4 | // import { WidgetConfig } from '../../isomorphic/types/widgetConfigs.types'; 5 | // import { EnvConfig } from '../../types'; 6 | // import { ClientFeatureFlags } from '../../types/featureFlags'; 7 | 8 | // export const ONE_DS_QE_PROD_ENVIRONMENTS: (typeof environment.environmentName)[] = 9 | // ['demo:1ds', 'production:1ds']; 10 | 11 | // /** 12 | // * 13 | // * @param page 14 | // * @param configName 15 | // * @returns configs from the of the page, e.g. the EnvConfig 16 | // */ 17 | // export const getOneDSConfigFromPage = async ( 18 | // page: Page, 19 | // configName: string, 20 | // ): Promise => { 21 | // return await page.evaluate((configName) => { 22 | // const oneDsConfig = 23 | // JSON.parse( 24 | // document?.querySelector(`script[data-1ds-config-id="${configName}"]`) 25 | // ?.innerHTML || '[]', 26 | // ) ?? []; 27 | // return oneDsConfig; 28 | // }, configName); 29 | // }; 30 | 31 | /** 32 | * Extracts widget config from the DOM using the widget.frame div that surrounds the mounted widget 33 | * @param page 34 | * @param widgetId 35 | * @returns 36 | */ 37 | export const getWidgetConfigFromPage = async ( 38 | page: Page, 39 | widgetId: string, 40 | ): Promise => { 41 | return await page.evaluate(async (widgetId: string) => { 42 | const widgetConfigs = document?.querySelector( 43 | `script[type="application/json"][data-1fe-config-id="widget-config"]`, 44 | ); 45 | 46 | try { 47 | const widgetConfig = JSON.parse(widgetConfigs?.innerHTML || '[]'); 48 | const widgetConfigForId = widgetConfig.find( 49 | (config: any) => config.widgetId === widgetId, 50 | ); 51 | return widgetConfigForId; 52 | } catch (error) { 53 | throw new Error( 54 | `Could not parse widget-config from the DOM for widgetId: ${widgetId}`, 55 | ); 56 | } 57 | }, widgetId); 58 | }; 59 | 60 | // export const isProdEnvironmentRun = ['production', 'demo'].includes( 61 | // process.env.NODE_ENV?.toLowerCase() || 'production', 62 | // ); 63 | 64 | // export const isIntegrationEnvironmentRun = [ 65 | // 'development', 66 | // 'integration', 67 | // ].includes(process.env.NODE_ENV?.toLowerCase() || 'production'); 68 | 69 | // export const getEnvConfigFromDom = async (page: Page): Promise => { 70 | // const envConfig = await page.evaluate( 71 | // () => 72 | // JSON.parse( 73 | // document?.querySelector(`script[data-1ds-config-id="env-config"]`) 74 | // ?.innerHTML ?? '{}', 75 | // ) as any, 76 | // ); 77 | 78 | // return envConfig; 79 | // }; 80 | 81 | // export const getEnvironmentFromDom = async ( 82 | // page: Page, 83 | // ): Promise => { 84 | // const envConfig = await getEnvConfigFromDom(page); 85 | // return envConfig.ENVIRONMENT; 86 | // }; 87 | 88 | // export const getFeatureFlagsFromDom = async ( 89 | // page: Page, 90 | // ): Promise => { 91 | // const envConfig = await getEnvConfigFromDom(page); 92 | // return envConfig.FEATURE_FLAGS; 93 | // }; 94 | -------------------------------------------------------------------------------- /src/shell/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { shellLogger } from '../logger'; 4 | import { OneFEErrorComponentProps } from '@1fe/shell'; 5 | 6 | const PageContainer = styled.div({ 7 | fontFamily: 'DSIndigo', 8 | margin: '4vh auto', 9 | width: '100%', 10 | maxWidth: '80vw', 11 | height: 'auto', 12 | display: 'flex', 13 | justifyContent: 'center', 14 | }); 15 | 16 | const AnimationContainer = styled.div({ 17 | height: '200px', 18 | width: '100%', 19 | display: 'flex', 20 | justifyContent: 'center', 21 | alignItems: 'center', 22 | '@media only screen and (max-width: 480px)': { 23 | line: { 24 | display: 'none', 25 | }, 26 | }, 27 | }); 28 | 29 | const MainText = styled.h1({ 30 | margin: 0, 31 | }); 32 | 33 | const SubText = styled.p({ 34 | fontSize: '1rem', 35 | lineHeight: 1.5, 36 | marginTop: '8px', 37 | }); 38 | 39 | type ErrorPageType = 'error' | 'notFound'; 40 | 41 | type ErrorPageData = { 42 | [key in ErrorPageType]: { 43 | titleText: string; 44 | subText: string; 45 | buttonText: string; 46 | }; 47 | }; 48 | 49 | type ErrorProps = { 50 | type?: ErrorPageType; 51 | plugin?: OneFEErrorComponentProps['plugin']; 52 | message?: string | undefined; 53 | }; 54 | 55 | // The SideLine is a SVG that helps keep the animation at the proper aspect ratio while still being responsive on larger screens. It looks like a continuation of the animation and is on both the left and right side of the animation 56 | const SideLine = () => { 57 | return ( 58 | 65 | ); 66 | }; 67 | 68 | export const Error = ({ type = 'error', plugin, message }: ErrorProps = {}) => { 69 | useEffect(() => { 70 | shellLogger.log({ 71 | message: '[1FE-Shell] error page rendered', 72 | errorComponent: { 73 | type, 74 | plugin, 75 | message, 76 | }, 77 | }); 78 | 79 | // We should flush this log right away, users will often navigate away after an error page and the request *might* get canceled 80 | // KazMon sdk flushes on beforeUnload events but this is not 100% reliable in all browsers and scenarios. 81 | // logger.flush(); 82 | }, []); 83 | 84 | const ErrorPageData: ErrorPageData = { 85 | error: { 86 | titleText: 'An error has occurred', 87 | subText: 'Make sure your connection is stable and try again', 88 | buttonText: 'Try Again', 89 | }, 90 | notFound: { 91 | titleText: 'Looks like this page is not here', 92 | subText: 'Check your URL, or go back', 93 | buttonText: 'Go Back', 94 | }, 95 | }; 96 | 97 | const mainText = message ?? ErrorPageData[type].titleText; 98 | const subText = ErrorPageData[type].subText; 99 | 100 | return ( 101 | <> 102 | 103 | 104 | 105 | 106 | 107 |
108 | {mainText} 109 | 110 | {subText} 111 |
112 |
113 | 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /bundle/client/prod.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require('copy-webpack-plugin'); 2 | const { resolve } = require('path'); 3 | const { EnvironmentPlugin } = require('webpack'); 4 | const merge = require('webpack-merge'); 5 | const swConfig = require('./sw'); 6 | const shellTypesConfig = require('./shell-types'); 7 | const { 8 | commonPlugins, 9 | shouldUseDevelopmentMode, 10 | getBrowserslistTargets, 11 | } = require('./utils'); 12 | 13 | const tsconfigClient = resolve(__dirname, '../../tsconfig.json'); 14 | 15 | /** 16 | * The production webpack configuration for the 1FE shell is also treated as the "common" configuration. 17 | * 18 | * @param configOverrides 19 | */ 20 | const getProdConfig = async (configOverrides) => { 21 | // Get dynamic browserslist target 22 | const browserslistConfig = await getBrowserslistTargets(); 23 | 24 | // Webpack read's browser list from the environment variable BROWSERSLIST 25 | process.env.BROWSERSLIST = browserslistConfig.join(); 26 | 27 | const prodConfig = { 28 | mode: shouldUseDevelopmentMode ? 'development' : 'production', 29 | target: 'browserslist', 30 | devtool: shouldUseDevelopmentMode ? 'source-map' : false, 31 | entry: { 32 | bundle: ['core-js', './src/shell/main.tsx'], 33 | }, 34 | output: { 35 | path: resolve(__dirname, '../../dist/public'), 36 | filename: 'js/[name].js', 37 | chunkFilename: 'js/chunk.[name].js', 38 | publicPath: 'auto', 39 | libraryTarget: 'system', 40 | library: { 41 | type: 'system', 42 | }, 43 | }, 44 | resolve: { 45 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 46 | }, 47 | optimization: { 48 | chunkIds: 'named', 49 | }, 50 | externals: { 51 | react: 'React', 52 | 'react-dom': 'ReactDOM', 53 | 'react-router': 'ReactRouter', 54 | 'react-router-dom': 'ReactRouterDOM', 55 | '@remix-run/router': 'RemixRouter', 56 | }, 57 | plugins: [ 58 | ...commonPlugins, 59 | new EnvironmentPlugin({ 60 | NODE_ENV: 'development', 61 | SYSTEM_ENVIRONMENT: true, 62 | IS_TEST_RUN: '0', 63 | }), 64 | new CopyPlugin({ 65 | patterns: [ 66 | { 67 | from: resolve(__dirname, '../../src/public'), 68 | to: resolve(__dirname, '../../dist/public'), 69 | }, 70 | ], 71 | }), 72 | ], 73 | module: { 74 | rules: [ 75 | { 76 | test: /\.tsx?$/, 77 | use: { 78 | loader: 'ts-loader', 79 | options: { 80 | transpileOnly: true, 81 | configFile: tsconfigClient, 82 | }, 83 | }, 84 | }, 85 | { 86 | test: /\.css$/, 87 | use: ['style-loader', 'css-loader'], 88 | }, 89 | { 90 | test: /\.[cm]?js$/, 91 | use: [ 92 | 'thread-loader', 93 | { 94 | loader: 'babel-loader', 95 | }, 96 | ], 97 | }, 98 | ], 99 | }, 100 | }; 101 | 102 | return [ 103 | merge.mergeWithCustomize({ plugins: 'replace' })( 104 | prodConfig, 105 | configOverrides, 106 | ), 107 | swConfig, 108 | shellTypesConfig, 109 | ]; 110 | }; 111 | 112 | module.exports = getProdConfig; 113 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/validate-headers-response.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Validation of security middleware headers for different routes', () => { 4 | const xPoweredByValue = '1FE Starter App'; 5 | const noStrictTransportSecurityValue = 'max-age=31536000; includeSubDomains;'; 6 | const noStoreValue = 'no-store'; 7 | 8 | const routes = [ 9 | 'health', 10 | 'version', 11 | 'app1', // for index route 12 | ]; 13 | routes.forEach((route: string) => { 14 | test(`set for -${route}`, async ({ page }) => { 15 | let headers: Record = {}; 16 | const url = 'http://localhost:3001'; 17 | const currenturl = new URL(url); 18 | page.on('response', (response) => { 19 | headers = response.headers(); 20 | return headers; 21 | }); 22 | await page.goto(`${currenturl.origin}/${route}`, { 23 | waitUntil: 'commit', // since we're interested only in the network response and page interaction not used in this test 24 | }); 25 | expect(headers['x-powered-by']).toBe(xPoweredByValue); 26 | expect(headers['cache-control']).toBe(noStoreValue); 27 | expect(headers['strict-transport-security']).toBe( 28 | noStrictTransportSecurityValue, 29 | ); 30 | }); 31 | }); 32 | 33 | test('Validate csp headers', async ({ page }) => { 34 | const url = 'http://localhost:3001'; 35 | const currenturl = new URL(url); 36 | 37 | const consoleErrors: string[] = []; 38 | 39 | page.on('console', (message) => { 40 | if ( 41 | message.type() === 'error' && 42 | message.text()?.includes('Content Security Policy') 43 | ) { 44 | consoleErrors.push(message.text()); 45 | } 46 | }); 47 | 48 | let cspViolationHeaders: Record = {}; 49 | 50 | page.on('response', async (response) => { 51 | if (response.url() === `${currenturl.origin}/csp-report-violation`) { 52 | cspViolationHeaders = response.headers(); 53 | } 54 | }); 55 | 56 | // Wait for the csp-report-only and csp-report-violation responses 57 | const cspReportViolationResponsePromise = page.waitForResponse( 58 | '**/csp-report-violation', 59 | ); 60 | 61 | await page.goto(`${currenturl.origin}/widget-starter-kit`, { 62 | waitUntil: 'domcontentloaded', 63 | }); 64 | 65 | // Fetch a URL that violates the csp policy 66 | const urlToFetch = 'https://jsonplaceholder.typicode.com/posts/1'; 67 | 68 | await page.evaluate(async (url) => { 69 | try { 70 | const res = await fetch(url); 71 | if (!res.ok) { 72 | throw new Error(`HTTP error! status: ${res.status}`); 73 | } 74 | return await res.json(); 75 | } catch (error: unknown) { 76 | return { error: (error as Error).message }; 77 | } 78 | }, urlToFetch); 79 | expect( 80 | consoleErrors[0].includes( 81 | `Refused to connect to '${urlToFetch}' because it violates the following Content Security Policy directive`, 82 | ), 83 | ).toBeTruthy(); 84 | 85 | await cspReportViolationResponsePromise; 86 | 87 | expect(cspViolationHeaders['x-powered-by']).toBe(xPoweredByValue); 88 | expect(cspViolationHeaders['cache-control']).toBe(noStoreValue); 89 | expect(cspViolationHeaders['strict-transport-security']).toBe( 90 | noStrictTransportSecurityValue, 91 | ); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@1fe/starter-app", 3 | "version": "0.0.4", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/docusign/1fe-starter-app.git" 7 | }, 8 | "types": "dist/shell/index.d.ts", 9 | "scripts": { 10 | "clean": "rimraf dist", 11 | "dev": "run-s client:dev server:dev", 12 | "client:dev": "npx webpack --config ./bundle/client/dev.js", 13 | "server:dev": "node bundle/server/dev.js", 14 | "client:build": "cross-env npx webpack --config ./bundle/client/build.js", 15 | "client-types": "npx tsc --project ./tsconfig-shell.types.json", 16 | "client-types:dev": "tsc-watch --project ./tsconfig-shell.types.json --onSuccess 'npx yalc push'", 17 | "client-types:extract": "yarn client-types && npx api-extractor run --local", 18 | "client-types:check": "yarn client-types && npx api-extractor run", 19 | "server:build": "tsc --project tsconfig.server.json --noEmit && node bundle/server/build.js", 20 | "build": "yarn clean && yarn server:build && yarn client:build", 21 | "test:playwright": "playwright test", 22 | "start": "node dist/server.js", 23 | "prepack": "yarn build" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.26.10", 27 | "@changesets/cli": "^2.29.5", 28 | "@es-exec/esbuild-plugin-start": "^0.0.5", 29 | "@playwright/test": "^1.51.1", 30 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", 31 | "@types/ejs": "^3.1.5", 32 | "@types/express": "^4.17.13", 33 | "@types/lodash": "^4.14.200", 34 | "@types/node": "22.9.0", 35 | "@types/react": "^18.2.62", 36 | "@types/react-dom": "^18.2.19", 37 | "@types/react-refresh": "^0", 38 | "@types/serve-favicon": "^2.5.7", 39 | "@types/systemjs": "^6.15.1", 40 | "@typescript-eslint/eslint-plugin": "^5.62.0", 41 | "@typescript-eslint/parser": "^5.62.0", 42 | "@vitejs/plugin-react-refresh": "^1.3.6", 43 | "babel-loader": "^9.2.1", 44 | "chalk": "^5.4.1", 45 | "concurrently": "^6.3.0", 46 | "copy-webpack-plugin": "^12.0.2", 47 | "cross-env": "^7.0.3", 48 | "css-loader": "^7.1.2", 49 | "esbuild": "0.19.5", 50 | "esbuild-plugin-copy": "^2.1.1", 51 | "eslint": "8.57.1", 52 | "eslint-config-prettier": "^10.1.1", 53 | "http-proxy-middleware": "^2.0.1", 54 | "lint-staged": "^13.3.0", 55 | "npm-run-all": "^4.1.5", 56 | "prettier": "^3.5.3", 57 | "progress-bar-webpack-plugin": "^2.1.0", 58 | "rimraf": "^6.0.1", 59 | "style-loader": "^4.0.0", 60 | "stylelint": "^14.0.0", 61 | "stylelint-config-prettier": "^9.0.3", 62 | "stylelint-config-standard": "^23.0.0", 63 | "thread-loader": "^4.0.4", 64 | "ts-loader": "^9.5.2", 65 | "ts-node-dev": "^2.0.0", 66 | "typescript": "5.8.2", 67 | "vite": "^6.2.3", 68 | "webpack": "^5.97.1", 69 | "webpack-cli": "^6.0.1", 70 | "webpack-dev-server": "^5.2.0", 71 | "webpack-merge": "^6.0.1" 72 | }, 73 | "dependencies": { 74 | "@1fe/server": "^0.1.2", 75 | "@1fe/shell": "^0.1.2", 76 | "@azure/app-configuration-provider": "^2.1.0", 77 | "@emotion/react": "^11.14.0", 78 | "@emotion/styled": "11.14.0", 79 | "core-js": "^3.40.0", 80 | "dotenv": "^16.0.3", 81 | "ejs": "^3.1.10", 82 | "express": "^4.21.2", 83 | "express-validator": "^7.0.1", 84 | "lodash": "^4.17.21", 85 | "react": "^18.3.1", 86 | "react-dom": "^18.3.1", 87 | "react-refresh": "^0.16.0", 88 | "react-router-dom": "6.30.0", 89 | "serve-favicon": "^2.5.0", 90 | "systemjs": "^6.15.1", 91 | "workbox-core": "^7.3.0", 92 | "workbox-expiration": "^7.3.0", 93 | "workbox-precaching": "^7.3.0", 94 | "workbox-strategies": "^7.3.0", 95 | "zod": "^3.25.36" 96 | }, 97 | "publishConfig": { 98 | "access": "public", 99 | "registry": "https://registry.npmjs.org/" 100 | }, 101 | "packageManager": "yarn@4.9.1" 102 | } 103 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/overrides/runtime-config-overrides.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { isEqual } from 'lodash'; 3 | 4 | import { getWidgetConfigFromPage } from '../../../test-utils/envs'; 5 | 6 | const wskId = '@1fe/widget-starter-kit'; 7 | 8 | const mockRuntimeConfigOverride = { 9 | [wskId]: { 10 | plugin: { 11 | // preload tags are automatically stripped out 12 | metaTags: [ 13 | { 14 | name: 'description', 15 | content: 'This is a dummy widget runtime config override', 16 | }, 17 | ], 18 | }, 19 | foo: 'bar', 20 | baz: 'qux', 21 | deepObject: { 22 | foo: 'bar', 23 | baz: 'qux', 24 | }, 25 | }, 26 | }; 27 | 28 | const wskRuntimeOverrideUrl = `http://localhost:3001/widget-starter-kit?runtime_config_overrides=${JSON.stringify(mockRuntimeConfigOverride)}`; 29 | 30 | test('"?runtime_config_overrides=" query param overrides the runtime config', async ({ 31 | page, 32 | }) => { 33 | await page.goto(wskRuntimeOverrideUrl); 34 | 35 | // check if meta tags are overridden and present in the response html 36 | for (const tag of mockRuntimeConfigOverride[wskId].plugin.metaTags) { 37 | const tagAttributes = Object.entries(tag).reduce( 38 | (itr, e) => `${itr}[${e[0]}="${e[1]}"]`, 39 | '', 40 | ); 41 | 42 | // expect to exist in the DOM 43 | // if (isIntegrationEnv) { 44 | expect(await page.$(`meta${tagAttributes}`)).not.toBeUndefined(); 45 | // } else { 46 | // expect(await page.$(`meta${tagAttributes}`)).toBeNull(); 47 | // } 48 | } 49 | 50 | const wskRuntimeConfig = await page.evaluate( 51 | ({ wskId }) => { 52 | const head = document.querySelector('head'); 53 | 54 | const widgetConfigsEl = head?.querySelector( 55 | 'script[type="application/json"][data-1fe-config-id="widget-config"]', 56 | )?.innerHTML; 57 | 58 | const parsedWidgetConfigs = JSON.parse(widgetConfigsEl as string); 59 | 60 | const wskConfig = parsedWidgetConfigs.find( 61 | (config: { widgetId: string }) => config.widgetId === wskId, 62 | ); 63 | 64 | const runtimeConfig = wskConfig?.runtime; 65 | 66 | return runtimeConfig; 67 | }, 68 | { wskId }, 69 | ); 70 | 71 | const { 72 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 73 | plugin, // plugin is stripped out 74 | ...restMockedRuntimeConfig 75 | } = mockRuntimeConfigOverride[wskId]; 76 | 77 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 78 | const { plugin: _, ...restRuntimeConfig } = wskRuntimeConfig; 79 | 80 | const isRuntimeConfigOverridden = isEqual( 81 | restMockedRuntimeConfig, 82 | restRuntimeConfig, 83 | ); 84 | 85 | // if (isIntegrationEnv) { 86 | expect(isRuntimeConfigOverridden).toBe(true); 87 | // } else { 88 | // expect(isRuntimeConfigOverridden).toBe(false); 89 | // } 90 | }); 91 | 92 | test.skip('runtime_config_overrides should take presidence over widget_url_overrides', async ({ 93 | page, 94 | }) => { 95 | const bundleOverrideUrl = 96 | 'https://1fe-a.akamaihd.net/integration/1fe/widgets/@internal/generic-child-widget/1.0.20/js/1fe-bundle.js'; 97 | 98 | const mockRuntimeConfigOverride = { 99 | preload: [ 100 | { 101 | apiGet: '/i-am-the-mock-override', 102 | }, 103 | ], 104 | }; 105 | 106 | const wskRuntimeAndBundleOverrideUrl = `http://localhost:3001/widget-starter-kit?runtime_config_overrides=${JSON.stringify({ ['@1fe/sample-widget']: mockRuntimeConfigOverride })}&widget_url_overrides=${JSON.stringify({ ['@1fe/sample-widget']: bundleOverrideUrl })}`; 107 | 108 | await page.goto(wskRuntimeAndBundleOverrideUrl); 109 | 110 | const runtimeConfig = ( 111 | await getWidgetConfigFromPage(page, '@1fe/sample-widget') 112 | )?.runtime; 113 | 114 | expect(runtimeConfig).toEqual(mockRuntimeConfigOverride); 115 | }); 116 | 117 | // test( 118 | // 'runtime_config_overrides is disabled on demo and prod', 119 | // async ({ page }) => { 120 | // const isIntegrationEnv = 121 | // environment.environmentName.includes('integration'); 122 | 123 | // const mockRuntimeConfigOverride = { 124 | // preload: [ 125 | // { 126 | // apiGet: '/i-am-the-mock-override', 127 | // }, 128 | // ], 129 | // }; 130 | 131 | // const launchUrl = environment.getOneDSLaunchUrl({ 132 | // shellUrl: SHELL_URL, 133 | // path: 'starter-kit', 134 | // additionalParams: { 135 | // [envConstants.ONEDS_TEST_URL_OVERRIDE_KEY]: environment.oneDSShellUrl, 136 | // [INTERNAL_PLUGIN_CODE]: INTERNAL_PLUGIN_GATE_CODE, 137 | // [RUNTIME_CONFIG_OVERRIDES]: JSON.stringify({ 138 | // ['@internal/generic-child-widget']: mockRuntimeConfigOverride, 139 | // }), 140 | // }, 141 | // }); 142 | 143 | // await page.goto(launchUrl); 144 | 145 | // const runtimeConfig = ( 146 | // await getWidgetConfigFromPage(page, '@internal/generic-child-widget') 147 | // )?.runtime; 148 | 149 | // if (isIntegrationEnv) { 150 | // expect(runtimeConfig).toEqual(mockRuntimeConfigOverride); 151 | // } else { 152 | // expect(runtimeConfig).not.toEqual(mockRuntimeConfigOverride); 153 | // } 154 | // }, 155 | // ); 156 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/overrides/import-map-overrides.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { 3 | /* overrideWidgetUrlWithUi, */ selectImportMapOverrideButton, 4 | } from '../../../test-utils/importMapOverridesUi'; 5 | // import { getWidgetConfigFromPage } from '../../../test-utils/envs'; 6 | // import { getLocalStorage } from '../../../test-utils/localStorage'; 7 | 8 | // const bundleOverrideUrl = 9 | // 'https://docutest-a.akamaihd.net/integration/1fe/widgets/@internal/generic-child-widget/1.0.20/js/1fe-bundle.js'; 10 | // const runtimeOverrideUrl = 11 | // 'https://docutest-a.akamaihd.net/integration/1fe/widgets/@internal/generic-child-widget/1.0.20/widget-runtime-config.json'; 12 | 13 | [, /*true*/ false].forEach((enable1dsDevtool) => { 14 | test( 15 | enable1dsDevtool 16 | ? 'If enable1dsDevtool is true, Import Map Overrides button visible on stage only @importMapOverrides @e2e' 17 | : 'If enable1dsDevtool is false, Import Map Overrides button visible on Int+Stage and not on Demo+Prod @importMapOverrides @e2e', 18 | async ({ page }) => { 19 | const url = 'http://localhost:3001/widget-starter-kit'; 20 | // const url = getWskUrlWithDevtoolVisible({ enable1dsDevtool }); 21 | await page.goto(url); 22 | 23 | const importMapOverrideButton = selectImportMapOverrideButton(page); 24 | 25 | // const environment = await getEnvironmentFromDom(page); 26 | 27 | // if (!enable1dsDevtool) { 28 | // if (isProductionEnvironment(environment)) { 29 | // await expect(importMapOverrideButton).toBeHidden(); 30 | // } else { 31 | await expect(importMapOverrideButton).toBeVisible(); 32 | // } 33 | 34 | // return; 35 | // } 36 | 37 | // if (environment === HOSTED_ENVIRONMENTS.stage) { 38 | // await expect(importMapOverrideButton).toBeVisible(); 39 | // } else { 40 | // await expect(importMapOverrideButton).toBeHidden(); 41 | // } 42 | }, 43 | ); 44 | 45 | // TODO[1fe][post-mvp]: re-enable this test when we have complex widget examples w/ runtime configs in child widget 46 | // test( 47 | // `enable1dsDevtool: ${enable1dsDevtool}, Using import-map-overrides ui triggers a runtime config override @importMapOverrides @e2e`, 48 | // async ({ page }) => { 49 | // const url = 'http://localhost:3001/widget-starter-kit'; 50 | // await page.goto(url); 51 | // // const environment = await getEnvironmentFromDom(page); 52 | 53 | // // if (isProductionEnvironment(environment)) { 54 | // // await expect(selectDevtoolImportMapOverrideButton(page)).toBeHidden(); 55 | // // await expect(selectImportMapOverrideButton(page)).toBeHidden(); 56 | // // return; 57 | // // } 58 | 59 | // const getGenericChildRuntimeConfig = async () => 60 | // (await getWidgetConfigFromPage(page, '@1fe/sample-widget')) 61 | // ?.runtime; 62 | 63 | // const startingRuntimeConfig = await getGenericChildRuntimeConfig(); 64 | 65 | // expect(startingRuntimeConfig).toEqual( 66 | // expectedCurrentGenericChildRuntimeConfig, 67 | // ); 68 | 69 | // const startingLocalStorage = await getLocalStorage(page); 70 | 71 | // expect(startingLocalStorage).toEqual([]); 72 | 73 | // let haveSeenRuntimeConfigFetch = false; 74 | 75 | // page.on('request', (request) => { 76 | // if (request.url() === runtimeOverrideUrl) { 77 | // haveSeenRuntimeConfigFetch = true; 78 | // } 79 | // }); 80 | 81 | // await overrideWidgetUrlWithUi({ 82 | // page, 83 | // widgetId: '@internal/generic-child-widget', 84 | // widgetBundleUrl: bundleOverrideUrl, 85 | // }); 86 | 87 | // expect(haveSeenRuntimeConfigFetch).toBe(true); 88 | 89 | // const endingLocalStorage = await getLocalStorage(page); 90 | 91 | // expect(endingLocalStorage).toEqual( 92 | // expectedLocalStorageForGenericChildOneOhTwenty, 93 | // ); 94 | 95 | // let haveSeenBundleFetch = false; 96 | 97 | // page.on('request', (request) => { 98 | // if (request.url() === bundleOverrideUrl) { 99 | // haveSeenBundleFetch = true; 100 | // } 101 | // }); 102 | 103 | // await widgetStarterKit.pages.header.goToUtilsPage(); 104 | 105 | // await page.reload(); 106 | 107 | // expect(haveSeenBundleFetch).toBe(false); 108 | 109 | // await widgetStarterKit.pages.utils.locator.widgetsGetBtn.click(); 110 | 111 | // expect(haveSeenBundleFetch).toBe(true); 112 | // }, 113 | // ); 114 | 115 | // test( 116 | // `enable1dsDevtool: ${enable1dsDevtool}, ?runtime_config_overrides - import map and localStorage runtime configs should be cleared when resetting @e2e`, 117 | // async ({ page }) => { 118 | // const url = 'http://localhost:3001/widget-starter-kit'; 119 | // await page.goto(url); 120 | // // const environment = await getEnvironmentFromDom(page); 121 | 122 | // // if (isProductionEnvironment(environment)) { 123 | // // await expect(selectDevtoolImportMapOverrideButton(page)).toBeHidden(); 124 | // // await expect(selectImportMapOverrideButton(page)).toBeHidden(); 125 | // // return; 126 | // // } 127 | 128 | // await overrideWidgetUrlWithUi({ 129 | // page, 130 | // widgetId: '@internal/generic-child-widget', 131 | // widgetBundleUrl: bundleOverrideUrl, 132 | // }); 133 | 134 | // const endingLocalStorage = await getLocalStorage(page); 135 | 136 | // expect(endingLocalStorage).toEqual( 137 | // expectedLocalStorageForGenericChildOneOhTwenty, 138 | // ); 139 | 140 | // await resetAllOverrides(page); 141 | 142 | // await page.reload(); 143 | 144 | // const clearedLocalStorage = await getLocalStorage(page); 145 | 146 | // expect(clearedLocalStorage).toEqual([]); 147 | // }, 148 | // ); 149 | }); 150 | -------------------------------------------------------------------------------- /src/sw.ts: -------------------------------------------------------------------------------- 1 | import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'; 2 | import { ExpirationPlugin } from 'workbox-expiration'; 3 | import { clientsClaim, skipWaiting } from 'workbox-core'; 4 | import { cleanupOutdatedCaches } from 'workbox-precaching'; 5 | const DOCUMENT_ASSETS_CACHE = 'DOCUMENT_ASSETS_CACHE'; 6 | const STATIC_ASSETS_CACHE = 'STATIC_ASSETS_CACHE'; 7 | 8 | // TODO - externalize this. 9 | // copied from @1fe/server because importing it causes sw.ts to use nodejs code somehow. 10 | const ONEFE_ROUTES = { 11 | HEALTH: '/health', 12 | VERSION: '/version', 13 | WIDGET_VERSION: '/version/*', 14 | INDEX: '/*', 15 | }; 16 | 17 | enum AssetType { 18 | DOCUMENTS = 'documents', 19 | STATIC_ASSETS = 'static-assets', 20 | } 21 | 22 | // Cache and retry only static assets 23 | const regexToCacheAndRetry = 24 | /^(?!.*(127\.0\.0\.1|localhost)).*https?:\/\/.*\.(js|css|woff|woff2|json|png|svg)$/; 25 | 26 | // non cache documents 27 | const DOCUMENTS_TO_NOT_CACHE: string[] = [ONEFE_ROUTES.HEALTH]; 28 | 29 | // 2xx, 3xx should be considered valid responses 30 | const isValidResponse = (responseStatus: number) => { 31 | return responseStatus >= 200 && responseStatus <= 399; 32 | }; 33 | 34 | // Force Service Worker Cache bust for documents (not js/css/font assets) 35 | const SW_CB = 'sw_cb'; 36 | 37 | // Retry twice after initial request failure 38 | const MAX_RETRIES = 2; 39 | 40 | const WIDGET_URL_OVERRIDES = 'widget_url_overrides'; 41 | 42 | const paramsNotToCache = [WIDGET_URL_OVERRIDES]; 43 | 44 | // Check cache first, then make request if not in cache 45 | const cacheFirstStaticAssets = new CacheFirst({ 46 | cacheName: STATIC_ASSETS_CACHE, 47 | plugins: [ 48 | new ExpirationPlugin({ 49 | maxAgeSeconds: 24 * 60 * 60 * 28, 50 | purgeOnQuotaError: true, 51 | }), 52 | ], 53 | }); 54 | 55 | const staleWhileRevalidateDocumentAssets = new StaleWhileRevalidate({ 56 | cacheName: DOCUMENT_ASSETS_CACHE, 57 | plugins: [ 58 | new ExpirationPlugin({ 59 | maxAgeSeconds: 5 * 60, // cache documents only for 5 minutes 60 | purgeOnQuotaError: true, 61 | maxEntries: 200, 62 | }), 63 | // Custom Plugin to cache only the first level path of the document 64 | // Cache only the first level path of the document 65 | // This is done to support MPAs where a unique document is served per pluginRoute (first level path) 66 | { 67 | cacheKeyWillBeUsed: async ({ request }) => { 68 | try { 69 | const incomingUrl = new URL(request.url); 70 | const pluginRoute = incomingUrl?.pathname?.split('/')?.[1] || ''; 71 | const cacheKeyByPluginRoute = new URL( 72 | pluginRoute, 73 | incomingUrl.origin, 74 | ); 75 | const pluginRequest = new Request(cacheKeyByPluginRoute.href, { 76 | mode: 'cors', 77 | credentials: 'omit', 78 | }); 79 | return pluginRequest; 80 | } catch (error) { 81 | // eslint-disable-next-line no-console 82 | console.error('Error creating cache key for document', error); 83 | return request; 84 | } 85 | }, 86 | }, 87 | ], 88 | }); 89 | 90 | // Handle fetch events 91 | self.addEventListener('fetch', (event: any) => { 92 | const incomingUrl = new URL(event.request.url); 93 | 94 | if ( 95 | event.request.mode === 'navigate' && // only catch document requests 96 | !incomingUrl.searchParams.has(SW_CB) && // SW cache bust requested on this request 97 | !DOCUMENTS_TO_NOT_CACHE.find((url) => 98 | incomingUrl.pathname.startsWith(url), 99 | ) && // incoming request is a document that should not be cached 100 | !paramsNotToCache.some((param) => incomingUrl.searchParams.get(param)) // incoming request is using param that needs server 101 | ) { 102 | // catch document requests 103 | event.respondWith(handleRequest(event, AssetType.DOCUMENTS)); 104 | } else if (event.request.url.match(regexToCacheAndRetry)) { 105 | // catch only static assets 106 | event.respondWith(handleRequest(event, AssetType.STATIC_ASSETS)); 107 | } else { 108 | return; 109 | } 110 | }); 111 | 112 | async function handleRequest(event: any, assetType: AssetType) { 113 | // Need cors to look at response status, otherwise response will be opaque 114 | const request = 115 | assetType === AssetType.DOCUMENTS 116 | ? new Request(event.request) 117 | : new Request(event.request, { 118 | mode: 'cors', 119 | credentials: 'omit', 120 | }); 121 | 122 | try { 123 | // cache.handle will throw an error if playwright simulates a timedout or aborted request 124 | const response = await (assetType === AssetType.DOCUMENTS 125 | ? staleWhileRevalidateDocumentAssets.handle({ event, request }) 126 | : cacheFirstStaticAssets.handle({ event, request })); 127 | 128 | // If response is good, bubble up to main app thread 129 | if (response && isValidResponse(response.status)) { 130 | return response; 131 | } 132 | } catch (error) { 133 | // eslint-disable-next-line no-console 134 | console.error(error); 135 | } 136 | 137 | // Else, initiate retry + fallback logic 138 | return handleRetry(request, assetType); 139 | } 140 | 141 | /** 142 | * When should we retry? 143 | * The fetch() function will automatically throw an error for network errors but not for HTTP errors such as 4xx or 5xx responses. 144 | * @param request 145 | * @returns 146 | */ 147 | const handleRetry = async (request: Request, assetType: AssetType) => { 148 | let retryCount = 0, 149 | response; 150 | 151 | while (retryCount < MAX_RETRIES) { 152 | try { 153 | response = await fetch(request); 154 | if (response && isValidResponse(response.status)) { 155 | // Request succeeded, return the response. 156 | return response; 157 | } 158 | } catch (error) { 159 | // eslint-disable-next-line no-console 160 | console.error(error); 161 | 162 | if (retryCount === MAX_RETRIES - 1 && assetType === AssetType.DOCUMENTS) { 163 | // If document fetch AND maximum retries reached, bubble original error 164 | throw error; 165 | } 166 | } 167 | 168 | retryCount++; 169 | } 170 | 171 | return response; 172 | }; 173 | 174 | // Bring parity with GenerateSW configurations 175 | clientsClaim(); 176 | cleanupOutdatedCaches(); 177 | skipWaiting(); 178 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/versions-contract-stablity.playwright.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { z } from 'zod'; 3 | 4 | const semverSchema = z.string().regex( 5 | // Matches semver versions with optional pre-release and build metadata 6 | /^v{0,1}\d+\.\d+\.\d+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$/, 7 | 'Must be a valid semver version', 8 | ); 9 | 10 | const widgetIdSchema = z.string().regex( 11 | // Matches widgets that follow the @org-name/package-name format 12 | /^@[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+$/, 13 | 'Must be a valid widgetId', 14 | ); 15 | 16 | const pluginRouteSchema = z.string().regex( 17 | // Matches routes that follow the /package-name format 18 | /^\/[a-zA-Z0-9-]+$/, 19 | 'Must be a valid plugin route', 20 | ); 21 | 22 | const packageSchema = z.strictObject({ 23 | id: z.string(), 24 | version: semverSchema, 25 | name: z.string(), 26 | }); 27 | 28 | const installedPackageSchema = z.strictObject({ 29 | id: z.string(), 30 | version: semverSchema, 31 | }); 32 | 33 | const widgetSchema = z.object({ 34 | widgetId: z.string(), 35 | version: z.string(), 36 | }); 37 | 38 | const pluginSchema = z.object({ 39 | enabled: z.boolean(), 40 | route: pluginRouteSchema, 41 | widgetId: widgetIdSchema, 42 | }); 43 | 44 | // TODO[1fe][post-mvp]: Adjust branch, gitSha, buildNumber when consuming back. 45 | const versionSchema = z.strictObject({ 46 | environment: z.string(), 47 | version: semverSchema, 48 | nodeVersion: semverSchema, 49 | // the next 3 fields are only available in non-prod environments 50 | buildNumber: z.string(), 51 | branch: z.string(), 52 | gitSha: z.string(), 53 | packages: z.object({ 54 | externals: z.array(packageSchema).nonempty(), 55 | installed: z.array(installedPackageSchema).nonempty(), 56 | }), 57 | configs: z.object({ 58 | widgetConfig: z.array(widgetSchema).nonempty(), 59 | pluginConfig: z.array(pluginSchema).nonempty(), 60 | }), 61 | hashOfWidgetConfigs: z.string(), 62 | }); 63 | 64 | // The versions endpoint is used by shell, CLI, widgets etc to determine the right version of libraries and widgets 65 | // Keeping the contract stable is critical to the 1ds ecosystem. Changes to the contract should be done with care. 66 | test('endpoint contract is stable @version', async ({ request }) => { 67 | const url = 'http://localhost:3001/version'; 68 | 69 | const response = await request.get(url); 70 | const versionJson = await response.json(); 71 | 72 | const result = versionSchema.parse(versionJson); 73 | expect(result).toBeDefined(); 74 | 75 | const externals = result.packages.externals.map( 76 | (e) => `${e.name}@${e.version}`, 77 | ); 78 | const installed = result.packages.installed.map( 79 | (e) => `${e.id}@${e.version}`, 80 | ); 81 | const widgetIds = result.configs.widgetConfig.map( 82 | (e) => `${e.widgetId}@${e.version}`, 83 | ); 84 | const pluginIds = result.configs.pluginConfig.map( 85 | (e) => `${e.widgetId} => ${e.route}`, 86 | ); 87 | 88 | expect(externals.length).toBeTruthy(); 89 | expect(installed.length).toBeTruthy(); 90 | expect(widgetIds.length).toBeTruthy(); 91 | expect(pluginIds.length).toBeTruthy(); 92 | }); 93 | 94 | test.describe('Version endpoint enhancements', () => { 95 | const widgetVersionSchema = z.strictObject({ 96 | id: widgetIdSchema, 97 | url: z.string(), 98 | version: semverSchema, 99 | bundle: z.string(), 100 | contract: z.string(), 101 | }); 102 | 103 | const createRequestUrl = (widgetId: string, version: string | undefined) => { 104 | return `http://localhost:3001/version/${widgetId}/${version}`; 105 | }; 106 | 107 | test.describe('Testing /version/:widgetId', () => { 108 | test('should return a 400 if the widgetId is invalid', async ({ 109 | request, 110 | }) => { 111 | const url = createRequestUrl('invalid/widget-id', '1.0.0'); 112 | const response = await request.get(url, { 113 | headers: { 114 | 'User-Agent': '1fe-automation', 115 | }, 116 | }); 117 | expect(response.status()).toBe(400); 118 | }); 119 | 120 | test('should return the "current" version of the widget', async ({ 121 | request, 122 | }) => { 123 | const url = createRequestUrl('@1fe/widget-starter-kit', 'current'); 124 | const response = await request.get(url, { 125 | headers: { 126 | 'User-Agent': '1fe-automation', 127 | }, 128 | }); 129 | expect(response.status()).toBe(200); 130 | 131 | const result = await response.json(); 132 | expect(result).toBeDefined(); 133 | 134 | const widgetVersion = widgetVersionSchema.parse(result); 135 | expect(widgetVersion).toBeDefined(); 136 | expect(widgetVersion.id).toBe('@1fe/widget-starter-kit'); 137 | expect(widgetVersion.url).toContain('@1fe/widget-starter-kit'); 138 | expect(widgetVersion.bundle).toContain('1fe-bundle.js'); 139 | expect(widgetVersion.contract).toContain('contract.rolledUp.d.ts'); 140 | }); 141 | }); 142 | 143 | test.describe('Testing /version/:widgetId/:version', () => { 144 | test('should return a 400 if using non-current version', async ({ 145 | request, 146 | }) => { 147 | const url = createRequestUrl('@1fe/widget-starter-kit', '1.0.0'); 148 | const response = await request.get(url, { 149 | headers: { 150 | 'User-Agent': '1fe-automation', 151 | }, 152 | }); 153 | 154 | expect(response.status()).toBe(400); 155 | }); 156 | }); 157 | 158 | test.describe('Testing /version/:widgetId/:version/bundle', () => { 159 | test('should return a 400 if using non-current version', async ({ 160 | request, 161 | }) => { 162 | const url = createRequestUrl('@1fe/widget-starter-kit', '1.0.0'); 163 | const response = await request.get(url, { 164 | headers: { 165 | 'User-Agent': '1fe-automation', 166 | }, 167 | }); 168 | expect(response.status()).toBe(400); 169 | }); 170 | 171 | test('should return the "current" version of the widget', async ({ 172 | request, 173 | }) => { 174 | const url = createRequestUrl('@1fe/widget-starter-kit', 'current'); 175 | const response = await request.get(`${url}/bundle`, { 176 | headers: { 177 | 'User-Agent': '1fe-automation', 178 | }, 179 | }); 180 | expect(response.status()).toBe(200); 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1fe Starter App 🚀 2 | 3 | A production-ready template for creating your own 1fe instance. This application serves as both the reference implementation powering [demo.1fe.com](https://demo.1fe.com) and as the git template used by `create-1fe-app`. 4 | 5 | ## 🎯 What is this? 6 | 7 | This starter app demonstrates how to build a complete 1fe instance using `@1fe/server` and `@1fe/shell`. It includes: 8 | 9 | - **Examples for live configurations** for live configurations, CSP settings, and environment management 10 | - **Two environments**: Integration and Production setups 11 | - **Example widgets** and plugin integrations 12 | 13 | ## 📋 Prerequisites 14 | 15 | - **Node.js** `>= 22` 16 | - **Yarn** (package manager) 17 | 18 | ## 🚀 Quick Start 19 | 20 | ### 1. Using create-1fe-app (Recommended) 21 | 22 | ```bash 23 | npx @1fe/create-1fe-app my-1fe-app 24 | cd my-1fe-app 25 | yarn install 26 | yarn dev 27 | ``` 28 | 29 | ### 2. Access Your Application by going to 30 | 31 | ## 📦 Project Structure 32 | 33 | ``` 34 | src/ 35 | ├── configs/ 36 | │ ├── ecosystem-configs.ts # Live configurations 37 | │ ├── critical-libs.ts # Critical library URLs 38 | │ └── env.ts # Environment configuration 39 | ├── csp-configs.ts # Content Security Policy settings 40 | ├── server.ts # Express server setup 41 | ├── shell/ # Shell components and utilities 42 | └── public/ # Static assets 43 | ``` 44 | 45 | ## ⚙️ Configuration 46 | 47 | ### Environment Variables 48 | 49 | Create a `.env` file based on `.env.example`: 50 | 51 | | Variable | Description | Default | 52 | | --------------------- | ---------------- | ------------- | 53 | | `PORT` | Server port | `3001` | 54 | | `NODE_ENV` | Environment mode | `development` | 55 | | `SERVER_BUILD_NUMBER` | Build identifier | `local` | 56 | 57 | ### Live Configurations 58 | 59 | Update `src/configs/ecosystem-configs.ts` to point to your CDN: 60 | 61 | ```typescript 62 | export const configManagement: OneFEConfigManagement = { 63 | widgetVersions: { 64 | url: 'https://your-cdn.com/configs/widget-versions.json', 65 | }, 66 | libraryVersions: { 67 | url: 'https://your-cdn.com/configs/lib-versions.json', 68 | }, 69 | dynamicConfigs: { 70 | url: 'https://your-cdn.com/configs/live.json', 71 | }, 72 | refreshMs: 30 * 1000, 73 | }; 74 | ``` 75 | 76 | ### CSP Configuration 77 | 78 | Modify `src/csp-configs.ts` to allow your CDN domains: 79 | 80 | ```typescript 81 | export const cspConfigs = { 82 | 'script-src': [ 83 | "'self'", 84 | "'unsafe-inline'", 85 | 'https://your-cdn.com', 86 | // ... other sources 87 | ], 88 | // ... other CSP directives 89 | }; 90 | ``` 91 | 92 | ## 🛠️ Development Commands 93 | 94 | ```bash 95 | # Start development server (client + server) 96 | yarn dev 97 | 98 | # Build for production 99 | yarn build 100 | 101 | # Start production server 102 | yarn start 103 | 104 | # Run tests 105 | yarn test 106 | 107 | # Type checking 108 | yarn typecheck 109 | 110 | # Linting 111 | yarn lint 112 | ``` 113 | 114 | ## 🌍 Deployment 115 | 116 | This starter app can be deployed to any platform that supports Node.js applications: 117 | 118 | ### Deploy to Render 119 | 120 | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/docusign/1fe/1fe-starter-app) 121 | 122 | ### Deploy to Heroku 123 | 124 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/docusign/1fe/1fe-starter-app) 125 | 126 | ### Deploy to Vercel 127 | 128 | ```bash 129 | yarn global add vercel 130 | vercel --prod 131 | ``` 132 | 133 | ### Deploy to Railway 134 | 135 | ```bash 136 | yarn global add @railway/cli 137 | railway deploy 138 | ``` 139 | 140 | ### Deploy to AWS/Azure/GCP 141 | 142 | Follow the respective platform documentation for Node.js applications. The built application is a standard Express.js server. 143 | 144 | ## 🔗 Widget Integration 145 | 146 | ### Adding New Widgets 147 | 148 | 1. **Build your widget** using [1fe-widget-starter-kit](https://github.com/docusign/1fe-widget-starter-kit) 149 | 2. **Deploy widget assets** to your CDN 150 | 3. **Update widget-versions.json** with your widget's information 151 | 4. **Configure the widget** in your live.json file 152 | 5. **Update CSP settings** if needed for new domains 153 | 154 | ### Example Widget Configuration 155 | 156 | ```json 157 | // In your live.json 158 | { 159 | "widgets": { 160 | "basePrefix": "https://your-cdn.com/widgets/", 161 | "configs": [ 162 | { 163 | "widgetId": "@your-org/your-widget", 164 | "plugin": { 165 | "enabled": true, 166 | "route": "/your-widget" 167 | } 168 | } 169 | ] 170 | } 171 | } 172 | ``` 173 | 174 | ## 🔧 Customization 175 | 176 | ### Branding & Styling 177 | 178 | - Update `src/shell/components/` for custom layout components 179 | - Modify CSS variables in your shell styles 180 | - Replace favicon and other assets in `src/public/` 181 | 182 | ### Adding Custom Utilities 183 | 184 | - Extend the shell utilities in `src/shell/utils/` 185 | - Update the platform props interface 186 | - Ensure new utilities are available to widgets via the sandbox 187 | 188 | ### Environment-Specific Configuration 189 | 190 | - Create environment-specific config files 191 | - Use environment variables for sensitive data 192 | - Set up different CDN endpoints per environment 193 | 194 | ## 🤝 Related Projects 195 | 196 | - **[1fe](https://github.com/docusign/1fe)** - Core 1fe packages and CLI tools 197 | - **[1fe-widget-starter-kit](https://github.com/docusign/1fe-widget-starter-kit)** - Template for building widgets 198 | - **[1fe-playground](https://github.com/docusign/1fe-playground)** - Development sandbox 199 | - **[1fe-ci-cd](https://github.com/docusign/1fe-ci-cd)** - CI/CD pipeline templates 200 | 201 | ## 📖 Documentation 202 | 203 | - **[1fe Documentation](https://1fe.com/)** - Complete platform documentation 204 | - **[Getting Started Guide](https://1fe.com/start-here)** - Step-by-step setup 205 | - **[Architecture Overview](https://1fe.com/main-concepts/what-is-1fe)** - How 1fe works 206 | - **[Deployment Guide](https://1fe.com/getting-started/deploy-poc)** - Production deployment 207 | 208 | ## 🐛 Troubleshooting 209 | 210 | ### Common Issues 211 | 212 | **CSP Errors**: Make sure your CDN domains are added to `csp-configs.ts` 213 | **Widget Loading Failures**: Verify your live configurations are accessible and valid 214 | **Build Errors**: Ensure all dependencies are installed and Node.js version is >= 22 215 | 216 | ### Getting Help 217 | 218 | - Check the [documentation](https://1fe.com/start-here/) 219 | - Search [existing issues](https://github.com/docusign/1fe/issues) 220 | - Ask questions in [GitHub Discussions](https://github.com/docusign/1fe/discussions) 221 | 222 | ## 📄 License 223 | 224 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 225 | 226 | --- 227 | 228 | **Ready to build your 1fe instance?** Check out our [comprehensive documentation](https://1fe.com/start-here/) or explore the [live demo](https://demo.1fe.com)! 229 | -------------------------------------------------------------------------------- /src/__tests__/tests/shell/overrides/widget-url-overrides.playwright.ts: -------------------------------------------------------------------------------- 1 | // TODO[1fe]: Re-enable these tests when we have permanent cdn urls for widget overrides 2 | // import { test, expect } from '@playwright/test'; 3 | // import { setUserAgentForWidgetUrlOverride } from '../../../test-utils/setUserAgentForWidgetUrlOverride'; 4 | // import { getLocalStorage } from '../../../test-utils/localStorage'; 5 | // import { getWidgetConfigFromPage } from '../../../test-utils/envs'; 6 | 7 | // const wskId = '@1ds/widget-starter-kit'; 8 | // const genericPinnedId = '@internal/generic-pinned-widget'; 9 | // const authId = '@lh/widget'; 10 | 11 | // const pluginsToTest: { route: string; widgetId: string }[] = [ 12 | // { route: 'widget-demo-auth-test', widgetId: authId }, 13 | // ]; 14 | 15 | // // This is an old version of WSK that is outdated and has old data-qa tags that have since been removed. 16 | // const widgetUrl = 17 | // 'https://docutest-a.akamaihd.net/integration/1ds/widgets/@1ds/widget-starter-kit/1.0.123/js/1ds-bundle.js'; 18 | 19 | // const genericVariantsUrl = 20 | // 'https://docutest-a.akamaihd.net/integration/1ds/widgets/@internal/generic-variants-widget/1.0.5/js/1ds-bundle.js'; 21 | 22 | // const genericPinnedUrl = 23 | // 'https://docutest-a.akamaihd.net/integration/1ds/widgets/@internal/generic-pinned-widget/1.0.10/js/1ds-bundle.js'; 24 | 25 | // /** 26 | // * Create a launch url with the widget url override query params 27 | // * @param widgetId - the ID of the widget you will be overriding 28 | // * @param path - the path you want to override on 29 | // * @returns - the launch url with attached override query params 30 | // */ 31 | // const getUrlWithOverride = ( 32 | // widgetId: string, 33 | // path: string, 34 | // overrideUrl?: string, 35 | // ): string => 36 | // `http://localhost:3001/widget-starter-kit${path}?widget_url_overrides={"${widgetId}":${ overrideUrl ? overrideUrl : widgetUrl }}`; 37 | 38 | // const getUrlNoOverride = (path: string): string => 39 | // `http://localhost:3001/widget-starter-kit${path}`; 40 | 41 | // test( 42 | // 'Make sure "?widget_url_overrides=" query param works', 43 | // async ({ page }) => { 44 | // const responsePromise = page.waitForResponse(widgetUrl); 45 | // await setUserAgentForWidgetUrlOverride(page); 46 | // await page.goto(getUrlWithOverride(wskId, 'starter-kit')); 47 | 48 | // const response = await responsePromise; 49 | // expect(response.status()).toBe(200); 50 | 51 | // await expect(page.getByTestId('wsk.page.home')).toBeVisible(); 52 | // await expect(page.getByTestId('wsk.page.root')).toBeHidden(); 53 | // }, 54 | // ); 55 | 56 | // test( 57 | // 'Make sure override persists after query param is removed @wsk @e2e', 58 | // async ({ page }) => { 59 | // const responsePromise = page.waitForResponse(widgetUrl); 60 | // await setUserAgentForWidgetUrlOverride(page); 61 | // await page.goto(getUrlWithOverride(wskId, 'starter-kit')); 62 | 63 | // const response = await responsePromise; 64 | // expect(response.status()).toBe(200); 65 | 66 | // await expect(page.getByTestId('wsk.page.home')).toBeVisible(); 67 | // await expect(page.getByTestId('wsk.page.root')).toBeHidden(); 68 | 69 | // const responsePromise2 = page.waitForResponse(widgetUrl); 70 | // await page.goto(getUrlNoOverride('starter-kit')); 71 | 72 | // const response2 = await responsePromise2; 73 | // expect(response2.status()).toBe(200); 74 | 75 | // await expect(page.getByTestId('wsk.page.home')).toBeVisible(); 76 | // await expect(page.getByTestId('wsk.page.root')).toBeHidden(); 77 | // }, 78 | // ); 79 | 80 | // test( 81 | // 'Make sure "?widget_url_overrides=" query param works after first loading the page without the query param @wsk @e2e', 82 | // async ({ page }) => { 83 | // await setUserAgentForWidgetUrlOverride(page); 84 | // await page.goto(getUrlNoOverride('starter-kit')); 85 | 86 | // await expect(page.getByTestId('wsk.page.root')).toBeVisible(); 87 | // // Double check that we are not seeing the old WSK content 88 | // await expect(page.getByTestId('wsk.page.home')).toBeHidden(); 89 | 90 | // const responsePromise = page.waitForResponse(widgetUrl); 91 | 92 | // await setUserAgentForWidgetUrlOverride(page); 93 | // await page.goto(getUrlWithOverride(wskId, 'starter-kit')); 94 | 95 | // const response = await responsePromise; 96 | // expect(response.status()).toBe(200); 97 | 98 | // await expect(page.getByTestId('wsk.page.home')).toBeVisible(); 99 | // await expect(page.getByTestId('wsk.page.root')).toBeHidden(); 100 | // }, 101 | // ); 102 | 103 | // // TODO[1fe][post-mvp]: Re-enable when we have pinned widget examples set up 104 | // // test( 105 | // // '?widget_url_overrides leads to correct pinned widget behavior @e2e', 106 | // // async ({ page }) => { 107 | // // await page.goto('http://localhost:3001/widget-starter-kit'); 108 | 109 | // // const startingLocalStorage = await getLocalStorage(page); 110 | // // expect(startingLocalStorage).toEqual([]); 111 | 112 | // // // TODO[1fe][post-mvp]: Add back when complex widget examples w/ runtime configurations are set up 113 | // // // expect( 114 | // // // (await getWidgetConfigFromPage(page, '@internal/generic-child-widget')) 115 | // // // ?.runtime, 116 | // // // ).toEqual(expectedCurrentGenericChildRuntimeConfig); 117 | 118 | // // const bundleOverrideUrl = 119 | // // 'https://docutest-a.akamaihd.net/integration/1ds/widgets/@internal/generic-child-widget/1.0.20/js/1ds-bundle.js'; 120 | 121 | // // const launchUrl = environment.getOneDSLaunchUrl({ 122 | // // shellUrl: SHELL_URL, 123 | // // path: 'starter-kit', 124 | // // additionalParams: { 125 | // // [envConstants.ONEDS_TEST_URL_OVERRIDE_KEY]: environment.oneDSShellUrl, 126 | // // [INTERNAL_PLUGIN_CODE]: INTERNAL_PLUGIN_GATE_CODE, 127 | // // [WIDGET_URL_OVERRIDES]: JSON.stringify({ 128 | // // ['@internal/generic-child-widget']: bundleOverrideUrl, 129 | // // }), 130 | // // }, 131 | // // }); 132 | // // await setUserAgentForWidgetUrlOverride(page); 133 | // // await page.goto(launchUrl); 134 | 135 | // // // http://localhost:8080/starter-kit?automated_test_framework=playwright&test_url_override=http%3A%2F%2Flocalhost%3A8080&internal_plugin_code=eeba3ef4ba25c99f47757&widget_url_overrides=%7B%22%40internal%2Fgeneric-child-widget%22%3A%22https%3A%2F%2Fdocutest-a.akamaihd.net%2Fintegration%2F1ds%2Fwidgets%2F%40internal%2Fgeneric-child-widget%2F1.0.20%2Fjs%2F1ds-bundle.js%22%7D 136 | 137 | // // const runtimeConfig = ( 138 | // // await getWidgetConfigFromPage(page, '@internal/generic-child-widget') 139 | // // )?.runtime; 140 | 141 | // // expect(runtimeConfig).toEqual(expectedOldGenericChildRuntimeConfig); 142 | 143 | // // const endingLocalStorage = await getLocalStorage(page); 144 | 145 | // // expect(endingLocalStorage).toEqual( 146 | // // expectedLocalStorageForGenericChildOneOhTwenty, 147 | // // ); 148 | 149 | // // await widgetStarterKit.pages.header.goToUtilsPage(); 150 | 151 | // // // expect url does not contain widget_url_overrides 152 | // // expect(page.url()).not.toContain(WIDGET_URL_OVERRIDES); 153 | 154 | // // // validate before and after reload for confidence 155 | // // const validateLocalStorageAndGenericChildVersion = async () => { 156 | // // expect(endingLocalStorage).toEqual( 157 | // // expectedLocalStorageForGenericChildOneOhTwenty, 158 | // // ); 159 | 160 | // // let haveSeenBundleFetch = false; 161 | 162 | // // page.on('request', (request) => { 163 | // // if (request.url() === bundleOverrideUrl) { 164 | // // haveSeenBundleFetch = true; 165 | // // } 166 | // // }); 167 | 168 | // // await widgetStarterKit.pages.header.goToUtilsPage(); 169 | 170 | // // expect(haveSeenBundleFetch).toBe(false); 171 | 172 | // // await widgetStarterKit.pages.utils.locator.widgetsGetBtn.click(); 173 | 174 | // // // eslint-disable-next-line playwright/no-networkidle 175 | // // await page.waitForLoadState('networkidle'); 176 | 177 | // // expect(haveSeenBundleFetch).toBe(true); 178 | // // }; 179 | 180 | // // await validateLocalStorageAndGenericChildVersion(); 181 | 182 | // // await page.reload(); 183 | 184 | // // await validateLocalStorageAndGenericChildVersion(); 185 | // // }, 186 | // // ); 187 | 188 | // test( 189 | // '?widget_url_overrides should not override if using non-whitelisted source @e2e', 190 | // async ({ page }) => { 191 | // await page.goto('http://localhost:3001/widget-starter-kit'); 192 | 193 | // const startingLocalStorage = await getLocalStorage(page); 194 | // expect(startingLocalStorage).toEqual([]); 195 | 196 | // expect( 197 | // (await getWidgetConfigFromPage(page, '@1fe/sample-widget')) 198 | // ?.runtime, 199 | // ).toEqual(expectedCurrentGenericChildRuntimeConfig); 200 | 201 | // const bundleOverrideUrl = 202 | // 'https://docutest-a.akamaihd.net@malicious-site.com/integration/1ds/widgets/@internal/generic-child-widget/1.0.20/js/1ds-bundle.js'; 203 | 204 | // const launchUrl = environment.getOneDSLaunchUrl({ 205 | // shellUrl: SHELL_URL, 206 | // path: 'starter-kit', 207 | // additionalParams: { 208 | // [envConstants.ONEDS_TEST_URL_OVERRIDE_KEY]: environment.oneDSShellUrl, 209 | // [INTERNAL_PLUGIN_CODE]: INTERNAL_PLUGIN_GATE_CODE, 210 | // [WIDGET_URL_OVERRIDES]: JSON.stringify({ 211 | // ['@internal/generic-child-widget']: bundleOverrideUrl, 212 | // }), 213 | // }, 214 | // }); 215 | // await setUserAgentForWidgetUrlOverride(page); 216 | // await page.goto(launchUrl); 217 | 218 | // const endingLocalStorage = await getLocalStorage(page); 219 | 220 | // const hasOverrideInLocalStorage = endingLocalStorage.some( 221 | // (item) => 222 | // item.name === 'import-map-override:@internal/generic-child-widget', 223 | // ); 224 | 225 | // // There should be zero overrides in local storage. 226 | // expect(hasOverrideInLocalStorage).toBe(false); 227 | // }, 228 | // ); 229 | 230 | // test( 231 | // '?widget_url_overrides - import map and localStorage runtime configs should be cleared when resetting', 232 | // async ({ page }) => { 233 | // if (isProdEnvironmentRun) { 234 | // const button = selectImportMapOverrideButton(page); 235 | // await expect(button).toBeHidden(); 236 | // return; 237 | // } 238 | 239 | // const bundleOverrideUrl = 240 | // 'https://docutest-a.akamaihd.net/integration/1ds/widgets/@internal/generic-child-widget/1.0.20/js/1ds-bundle.js'; 241 | 242 | // const launchUrl = environment.getOneDSLaunchUrl({ 243 | // shellUrl: SHELL_URL, 244 | // path: 'starter-kit', 245 | // additionalParams: { 246 | // [envConstants.ONEDS_TEST_URL_OVERRIDE_KEY]: environment.oneDSShellUrl, 247 | // [INTERNAL_PLUGIN_CODE]: INTERNAL_PLUGIN_GATE_CODE, 248 | // [WIDGET_URL_OVERRIDES]: JSON.stringify({ 249 | // ['@internal/generic-child-widget']: bundleOverrideUrl, 250 | // }), 251 | // }, 252 | // }); 253 | 254 | // const urlWithoutAutomatedTestFramework = new URL(`${launchUrl}`); 255 | // urlWithoutAutomatedTestFramework.searchParams.delete( 256 | // 'automated_test_framework', 257 | // ); 258 | // await setUserAgentForWidgetUrlOverride(page); 259 | // await page.goto(urlWithoutAutomatedTestFramework.toString()); 260 | 261 | // const validateLocalStorage = async () => { 262 | // const endingLocalStorage = await getLocalStorage(page); 263 | 264 | // expect(endingLocalStorage).toEqual( 265 | // expectedLocalStorageForGenericChildOneOhTwenty, 266 | // ); 267 | // }; 268 | 269 | // await validateLocalStorage(); 270 | 271 | // urlWithoutAutomatedTestFramework.searchParams.delete(WIDGET_URL_OVERRIDES); 272 | // await setUserAgentForWidgetUrlOverride(page); 273 | // await page.goto(urlWithoutAutomatedTestFramework.toString()); 274 | 275 | // await validateLocalStorage(); 276 | 277 | // await resetAllOverrides(page); 278 | 279 | // await page.reload(); 280 | 281 | // const clearedLocalStorage = await getLocalStorage(page); 282 | 283 | // expect(clearedLocalStorage).toEqual([]); 284 | // }, 285 | // ); 286 | 287 | // // TODO[1fe][post-mvp]: Re-enable when we have variant widget support and examples set up 288 | // // test( 289 | // // 'Make sure "?widget_url_overrides=" query param should work with a default variant bundle @e2e', 290 | // // async ({ page }) => { 291 | // // const responsePromise = page.waitForResponse(genericVariantsUrl); 292 | // // await setUserAgentForWidgetUrlOverride(page); 293 | // // await page.goto( 294 | // // getUrlWithOverride(wskId, 'starter-kit', genericVariantsUrl), 295 | // // ); 296 | 297 | // // const response = await responsePromise; 298 | // // expect(response.status()).toBe(200); 299 | 300 | // // await expect(page.getByTestId('wsk.page.home')).toBeHidden(); 301 | 302 | // // await page 303 | // // .getByTestId('generic.child.utils.widgets.get.variant.btn') 304 | // // .click(); 305 | 306 | // // await expect( 307 | // // page.getByTestId('generic.widget.variant.on.page'), 308 | // // ).toBeVisible(); 309 | // // }, 310 | // // ); 311 | 312 | // // TODO[1fe][post-mvp]: Re-enable when we have pinned widget examples set up 313 | // // test( 314 | // // 'Using "?widget_url_overrides=" query param should respect overrided version when pinning widget @e2e', 315 | // // async ({ page }) => { 316 | // // await setUserAgentForWidgetUrlOverride(page); 317 | // // await page.goto( 318 | // // getUrlWithOverride( 319 | // // genericPinnedId, 320 | // // 'starter-kit/utils', 321 | // // genericPinnedUrl, 322 | // // ), 323 | // // ); 324 | 325 | // // await page.getByTestId('utils.widgets.get.btn').click(); 326 | // // await page 327 | // // .getByTestId('generic.child.utils.widgets.get.btn-pinned') 328 | // // .click(); 329 | 330 | // // const pinnedWidgetInnerText = await page 331 | // // .getByTestId('pinned.widget.data') 332 | // // .innerText(); 333 | 334 | // // const pinnedWidgetData = JSON.parse(pinnedWidgetInnerText); 335 | 336 | // // expect(pinnedWidgetData.packageJsonVersion).toBe('1.0.10'); 337 | // // }, 338 | // // ); 339 | --------------------------------------------------------------------------------