diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 912f26a32b..d80876121f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,14 +30,17 @@ jobs: pwa-kit: strategy: matrix: - node: [14] + node: [14, 16] npm: [6, 7, 8] + exclude: + - node: 16 + npm: 6 runs-on: ubuntu-latest env: - # The "default" npm is the one that ships with a given version of node - # node v14 uses npm@6, latest node v16 uses npm@8 + # The "default" npm is the one that ships with a given version of node. # For more: https://nodejs.org/en/download/releases/ - IS_DEFAULT_NPM: ${{ matrix.npm == 6 }} + IS_DEFAULT_NPM: ${{ matrix.node == 14 && matrix.npm == 6 || matrix.node == 16 && matrix.npm == 8 }} + IS_LATEST_NPM: ${{ matrix.node == 16 && matrix.npm == 8 }} steps: - name: Checkout uses: actions/checkout@v3 @@ -68,28 +71,28 @@ jobs: uses: "./.github/actions/smoke_tests" - name: Create MRT credentials file - if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.DEVELOP == 'true' + if: env.IS_NOT_FORK == 'true' && env.IS_LATEST_NPM == 'true' && env.DEVELOP == 'true' uses: "./.github/actions/create_mrt" with: mobify_user: ${{ secrets.MOBIFY_CLIENT_USER }} mobify_api_key: ${{ secrets.MOBIFY_CLIENT_API_KEY }} - name: Push Bundle to MRT (Development) - if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.DEVELOP == 'true' + if: env.IS_NOT_FORK == 'true' && env.IS_LATEST_NPM == 'true' && env.DEVELOP == 'true' uses: "./.github/actions/push_to_mrt" with: CWD: "./packages/template-retail-react-app" TARGET: staging - name: Push Bundle to MRT (Production) - if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.RELEASE == 'true' + if: env.IS_NOT_FORK == 'true' && env.IS_LATEST_NPM == 'true' && env.RELEASE == 'true' uses: "./.github/actions/push_to_mrt" with: CWD: "./packages/template-retail-react-app" TARGET: production - name: Push Bundle to MRT (Commerce SDK React) - if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.DEVELOPMENT == 'true' + if: env.IS_NOT_FORK == 'true' && env.IS_LATEST_NPM == 'true' && env.DEVELOPMENT == 'true' uses: "./.github/actions/push_to_mrt" with: CWD: "./packages/test-commerce-sdk-react" @@ -100,7 +103,7 @@ jobs: uses: "./.github/actions/check_clean" - name: Publish to NPM - if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.RELEASE == 'true' + if: env.IS_NOT_FORK == 'true' && env.IS_LATEST_NPM == 'true' && env.RELEASE == 'true' uses: "./.github/actions/publish_to_npm" with: NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} @@ -118,11 +121,16 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} pwa-kit-windows: strategy: - # TODO: We don't *need* a matrix with single values, - # but is it worth keeping for supporting multiple versions in the future? - matrix: - node: [14] - npm: [6] + matrix: + node: [14, 16] + npm: [6, 7, 8] + exclude: + - node: 16 + npm: 6 + env: + # The "default" npm is the one that ships with a given version of node. + # For more: https://nodejs.org/en/download/releases/ + IS_DEFAULT_NPM: ${{ matrix.node == 14 && matrix.npm == 6 || matrix.node == 16 && matrix.npm == 8 }} runs-on: windows-latest steps: - name: Checkout @@ -134,6 +142,11 @@ jobs: node-version: ${{ matrix.node }} cache: npm + - name: Update NPM version + if: env.IS_DEFAULT_NPM == 'false' + run: |- + npm install -g npm@${{ matrix.npm }} + - name: Setup Windows Machine uses: "./.github/actions/setup_windows" @@ -157,7 +170,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 16 - name: Setup Ubuntu Machine uses: "./.github/actions/setup_ubuntu" @@ -247,7 +260,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 16 - name: Setup Windows Machine uses: "./.github/actions/setup_windows" diff --git a/package.json b/package.json index 31bbbb1791..876af3d82a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pwa-kit", "version": "2.7.0-dev", "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "devDependencies": { diff --git a/packages/commerce-sdk-react/package.json b/packages/commerce-sdk-react/package.json index c2550a9dbe..5ba33efaeb 100644 --- a/packages/commerce-sdk-react/package.json +++ b/packages/commerce-sdk-react/package.json @@ -5,7 +5,7 @@ "author": "cc-pwa-kit@salesforce.com", "license": "See license in LICENSE", "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "files": [ diff --git a/packages/internal-lib-build/configs/babel.config.js b/packages/internal-lib-build/configs/babel.config.js index 79e63e0f08..30df8e4686 100644 --- a/packages/internal-lib-build/configs/babel.config.js +++ b/packages/internal-lib-build/configs/babel.config.js @@ -10,7 +10,7 @@ const config = { require('@babel/preset-env'), { targets: { - node: 14 + node: 16 } } ], diff --git a/packages/internal-lib-build/package.json b/packages/internal-lib-build/package.json index 84567f3f8e..315328d57c 100644 --- a/packages/internal-lib-build/package.json +++ b/packages/internal-lib-build/package.json @@ -2,7 +2,7 @@ "name": "internal-lib-build", "version": "2.7.0-dev", "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "private": true, diff --git a/packages/pwa-kit-create-app/README.md b/packages/pwa-kit-create-app/README.md index 1ac70838b0..a4ca55f0c4 100644 --- a/packages/pwa-kit-create-app/README.md +++ b/packages/pwa-kit-create-app/README.md @@ -4,7 +4,7 @@ A tool for generating PWA Kit projects based on project templates, such as the [ ## Requirements -- Node 14 +- Node 14.17.0 or later - npm 6.14.4 or later ## Quick Start diff --git a/packages/pwa-kit-create-app/assets/pwa/default.js b/packages/pwa-kit-create-app/assets/pwa/default.js index 687722489d..c5ff0b353d 100644 --- a/packages/pwa-kit-create-app/assets/pwa/default.js +++ b/packages/pwa-kit-create-app/assets/pwa/default.js @@ -65,7 +65,7 @@ module.exports = { ], // Additional parameters that configure Express app behavior. ssrParameters: { - ssrFunctionNodeVersion: '14.x', + ssrFunctionNodeVersion: '16.x', proxyConfigs: [ { host: '${commerceApi.shortCode}.api.commercecloud.salesforce.com', diff --git a/packages/pwa-kit-create-app/package.json b/packages/pwa-kit-create-app/package.json index f63aa17c92..a367eafb40 100644 --- a/packages/pwa-kit-create-app/package.json +++ b/packages/pwa-kit-create-app/package.json @@ -26,7 +26,7 @@ "test": "internal-lib-build test" }, "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "dependencies": { diff --git a/packages/pwa-kit-create-app/scripts/create-mobify-app-dev.js b/packages/pwa-kit-create-app/scripts/create-mobify-app-dev.js index d7f47d2821..720dec503c 100755 --- a/packages/pwa-kit-create-app/scripts/create-mobify-app-dev.js +++ b/packages/pwa-kit-create-app/scripts/create-mobify-app-dev.js @@ -45,6 +45,7 @@ const p = require('path') const sh = require('shelljs') const fs = require('fs') const cp = require('child_process') +const semver = require('semver') sh.set('-e') @@ -147,7 +148,12 @@ const runGenerator = () => { // Shelljs can't run interactive programs, so we have to switch to child_process. // See https://github.com/shelljs/shelljs/wiki/FAQ#running-interactive-programs-with-exec - cp.execSync(`npx pwa-kit-create-app ${process.argv.slice(2).join(' ')}`, { + const extension = process.platform === 'win32' ? '.cmd' : '' + const npm = `npm${extension}` + const foundNpm = cp.spawnSync(npm, ['-v']).stdout.toString().trim() + const flags = semver.satisfies(foundNpm, '>=7') ? '-y' : '' + + cp.execSync(`npx ${flags} pwa-kit-create-app@latest ${process.argv.slice(2).join(' ')}`, { stdio: 'inherit' }) } diff --git a/packages/pwa-kit-dev/CHANGELOG.md b/packages/pwa-kit-dev/CHANGELOG.md index 13f15ccd76..c720c21eb8 100644 --- a/packages/pwa-kit-dev/CHANGELOG.md +++ b/packages/pwa-kit-dev/CHANGELOG.md @@ -1,4 +1,6 @@ ## v2.7.0-dev (Jan 25, 2023) +- Add explicit `ws` dependency [#865](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/865) + ## v2.6.0 (Jan 25, 2023) - Upgrade prettier to v2 [#926](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/926) - Security package updates diff --git a/packages/pwa-kit-dev/README.md b/packages/pwa-kit-dev/README.md index 5bde8cbb9c..d718fa2bc3 100644 --- a/packages/pwa-kit-dev/README.md +++ b/packages/pwa-kit-dev/README.md @@ -4,7 +4,7 @@ A command-line tool to develop, build, and deploy PWA Kit projects. ## Requirements -- Node 14 +- Node 14.17.0 or later - npm 6.14.4 or later To see all the available commands, run: diff --git a/packages/pwa-kit-dev/package-lock.json b/packages/pwa-kit-dev/package-lock.json index 36a39326b8..4e811a93d3 100644 --- a/packages/pwa-kit-dev/package-lock.json +++ b/packages/pwa-kit-dev/package-lock.json @@ -6849,6 +6849,13 @@ "whatwg-url": "^8.5.0", "ws": "^7.4.6", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==" + } } }, "jsesc": { @@ -10722,6 +10729,11 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==" } } }, @@ -11099,9 +11111,9 @@ } }, "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==" + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", + "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==" }, "xml-name-validator": { "version": "3.0.0", diff --git a/packages/pwa-kit-dev/package.json b/packages/pwa-kit-dev/package.json index e29d7367c2..6ec7600caf 100644 --- a/packages/pwa-kit-dev/package.json +++ b/packages/pwa-kit-dev/package.json @@ -104,7 +104,8 @@ "webpack-dev-middleware": "^5.2.2", "webpack-hot-middleware": "^2.25.1", "webpack-hot-server-middleware": "^0.6.1", - "webpack-notifier": "^1.12.0" + "webpack-notifier": "^1.12.0", + "ws": "^8.12.0" }, "devDependencies": { "@loadable/component": "^5.15.0", @@ -117,7 +118,7 @@ "@loadable/component": "^5.15.0" }, "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "publishConfig": { diff --git a/packages/pwa-kit-dev/src/configs/babel/babel-config.js b/packages/pwa-kit-dev/src/configs/babel/babel-config.js index 604da99a28..d42e41c9fe 100644 --- a/packages/pwa-kit-dev/src/configs/babel/babel-config.js +++ b/packages/pwa-kit-dev/src/configs/babel/babel-config.js @@ -11,7 +11,7 @@ const config = { require('@babel/preset-env'), { targets: { - node: 14 + node: 16 } } ], diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index b43cc9ceb8..9e2036f55b 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -9,7 +9,7 @@ // For more information on these settings, see https://webpack.js.org/configuration import fs from 'fs' -import path, {resolve} from 'path' +import {resolve} from 'path' import webpack from 'webpack' import WebpackNotifierPlugin from 'webpack-notifier' @@ -23,7 +23,6 @@ import {createModuleReplacementPlugin} from './plugins' import {CLIENT, SERVER, CLIENT_OPTIONAL, SSR, REQUEST_PROCESSOR} from './config-names' const projectDir = process.cwd() -const sdkDir = resolve(path.join(__dirname, '..', '..', '..')) const pkg = require(resolve(projectDir, 'package.json')) const buildDir = process.env.PWA_KIT_BUILD_DIR @@ -65,8 +64,21 @@ const entryPointExists = (segments) => { } const findInProjectThenSDK = (pkg) => { - const projectPath = resolve(projectDir, 'node_modules', pkg) - return fs.existsSync(projectPath) ? projectPath : resolve(sdkDir, 'node_modules', pkg) + // Look for the SDK node_modules in two places because in CI, + // pwa-kit-dev is published under a 'dist' directory, which + // changes this file's location relative to the package root. + const candidates = [ + resolve(projectDir, 'node_modules', pkg), + resolve(__dirname, '..', '..', 'node_modules', pkg), + resolve(__dirname, '..', '..', '..', 'node_modules', pkg) + ] + let candidate + for (candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate + } + } + return candidate } const baseConfig = (target) => { diff --git a/packages/pwa-kit-dev/src/utils/script-utils.ts b/packages/pwa-kit-dev/src/utils/script-utils.ts index 3bbd53620d..53cdc1f492 100644 --- a/packages/pwa-kit-dev/src/utils/script-utils.ts +++ b/packages/pwa-kit-dev/src/utils/script-utils.ts @@ -119,6 +119,11 @@ export class CloudAPIClient { error = {} // Cloud doesn't always return JSON } + if (res.status === 403) { + error.docs_url = + 'https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/mrt-overview.html#users,-abilities,-and-roles' + } + throw new Error( [ `HTTP ${res.status}`, diff --git a/packages/pwa-kit-react-sdk/README.md b/packages/pwa-kit-react-sdk/README.md index a3af8fbecd..b3d8dd4d8e 100644 --- a/packages/pwa-kit-react-sdk/README.md +++ b/packages/pwa-kit-react-sdk/README.md @@ -6,7 +6,7 @@ A library of components and utilities that supports the rendering pipeline for t ## Requirements -- Node 14 +- Node 14.17.0 or later - npm 6.14.4 or later ## Install Dependencies diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index 49d3b19fcf..5e5ddca286 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -3,7 +3,7 @@ "version": "2.7.0-dev", "description": "A library that supports the isomorphic React rendering pipeline for Commerce Cloud Managed Runtime apps", "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "files": [ diff --git a/packages/pwa-kit-react-sdk/setup-jest.js b/packages/pwa-kit-react-sdk/setup-jest.js index d3f023ab1b..a485071a2c 100644 --- a/packages/pwa-kit-react-sdk/setup-jest.js +++ b/packages/pwa-kit-react-sdk/setup-jest.js @@ -31,7 +31,7 @@ jest.mock('pwa-kit-runtime/utils/ssr-config', () => { '**/*.json' ], ssrParameters: { - ssrFunctionNodeVersion: '14.x', + ssrFunctionNodeVersion: '16.x', proxyConfigs: [ { host: 'kv7kzm78.api.commercecloud.salesforce.com', diff --git a/packages/pwa-kit-runtime/CHANGELOG.md b/packages/pwa-kit-runtime/CHANGELOG.md index bf11dd14c2..d062caef2f 100644 --- a/packages/pwa-kit-runtime/CHANGELOG.md +++ b/packages/pwa-kit-runtime/CHANGELOG.md @@ -1,4 +1,6 @@ ## v2.7.0-dev (Jan 25, 2023) +- Support Node 16 [#965](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/965) + ## v2.6.0 (Jan 25, 2023) - Security package updates diff --git a/packages/pwa-kit-runtime/package.json b/packages/pwa-kit-runtime/package.json index f6a1d96d5f..f233872ce1 100644 --- a/packages/pwa-kit-runtime/package.json +++ b/packages/pwa-kit-runtime/package.json @@ -70,7 +70,7 @@ } }, "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "publishConfig": { diff --git a/packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js b/packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js index 44760afdb5..91e33b2098 100644 --- a/packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js +++ b/packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js @@ -19,7 +19,6 @@ import { isRemote, MetricsSender, outgoingRequestHook, - PerformanceTimer, processLambdaResponse, responseSend, configureProxyConfigs, @@ -495,15 +494,11 @@ export const RemoteServerFactory = { locals.afterResponseCalled = false locals.responseCaching = {} - locals.timer = new PerformanceTimer(`req${locals.requestId}`) locals.originalUrl = req.originalUrl // Track this response req.app._requestMonitor._responseStarted(res) - // Start timing - locals.timer.start('express-overall') - // If the path is /, we enforce that the only methods // allowed are GET, HEAD or OPTIONS. This is a restriction // imposed by API Gateway: we enforce it here so that the @@ -519,8 +514,6 @@ export const RemoteServerFactory = { const afterResponse = () => { /* istanbul ignore else */ if (!locals.afterResponseCalled) { - locals.timer.end('express-overall') - locals.timingResponse && locals.timer.end('express-response') locals.afterResponseCalled = true // Emit timing unless the request is for a proxy // or bundle path. We don't want to emit metrics @@ -547,9 +540,6 @@ export const RemoteServerFactory = { } req.app.sendMetric(metricName) } - locals.timer.finish() - // Release reference to timer - locals.timer = null } } @@ -969,16 +959,12 @@ const prepNonProxyRequest = (req, res, next) => { * @private */ const ssrMiddleware = (req, res, next) => { - const timer = res.locals.timer - timer.start('ssr-overall') - setDefaultHeaders(req, res) const renderStartTime = Date.now() const done = () => { const elapsedRenderTime = Date.now() - renderStartTime req.app.sendMetric('RenderTime', elapsedRenderTime, 'Milliseconds') - timer.end('ssr-overall') } res.on('finish', done) diff --git a/packages/pwa-kit-runtime/src/ssr/server/express.js b/packages/pwa-kit-runtime/src/ssr/server/express.js index c27b8bd891..fe70ace375 100644 --- a/packages/pwa-kit-runtime/src/ssr/server/express.js +++ b/packages/pwa-kit-runtime/src/ssr/server/express.js @@ -306,14 +306,10 @@ export const cacheResponseWhenDone = ({ // We know that all the data has been written, so we // can now store the response in the cache and call // end() on it. - const timer = res.locals.timer req.app.applicationCache._cacheDeletePromise .then(() => { localDevLog(`Req ${locals.requestId}: caching response for ${req.url}`) - timer.start('cache-response') - return storeResponseInCache(req, res).then(() => { - timer.end('cache-response') - }) + return storeResponseInCache(req, res) }) .finally(() => { originalEnd.call(res, callback) @@ -390,11 +386,7 @@ export const getResponseFromCache = ({req, res, namespace, key}) => { locals.responseCaching.cacheKey = workingKey // Return a Promise that handles the asynchronous cache lookup - const timer = res.locals.timer - timer.start('check-response-cache') return req.app.applicationCache.get({key: workingKey, namespace}).then((entry) => { - timer.end('check-response-cache') - localDevLog( `Req ${locals.requestId}: ${ entry.found ? 'Found' : 'Did not find' diff --git a/packages/pwa-kit-runtime/src/utils/ssr-server.js b/packages/pwa-kit-runtime/src/utils/ssr-server.js index 4142492624..b7302194fd 100644 --- a/packages/pwa-kit-runtime/src/utils/ssr-server.js +++ b/packages/pwa-kit-runtime/src/utils/ssr-server.js @@ -16,7 +16,6 @@ export * from './ssr-server/detect-device-type' export * from './ssr-server/metrics-sender' export * from './ssr-server/outgoing-request-hook' export * from './ssr-server/parse-end-parameters' -export * from './ssr-server/performance-timer' export * from './ssr-server/process-express-response' export * from './ssr-server/process-lambda-response' export * from './ssr-server/update-global-agent-options' diff --git a/packages/pwa-kit-runtime/src/utils/ssr-server.test.js b/packages/pwa-kit-runtime/src/utils/ssr-server.test.js index 3c15b0d18d..710affd713 100644 --- a/packages/pwa-kit-runtime/src/utils/ssr-server.test.js +++ b/packages/pwa-kit-runtime/src/utils/ssr-server.test.js @@ -21,7 +21,6 @@ import { outgoingRequestHook, parseCacheControl, parseEndParameters, - PerformanceTimer, processExpressResponse, processLambdaResponse, updateGlobalAgentOptions, @@ -960,41 +959,6 @@ describe('updateGlobalAgentOptions', () => { }) }) -describe('PerformanceTimer tests', () => { - let timer - beforeEach(() => (timer = new PerformanceTimer(''))) - afterEach(() => timer?._observer.disconnect()) - - test('time() function', () => { - const func = sinon.stub() - timer.time('test1', func, 1) - expect(func.callCount).toBe(1) - expect(func.calledWith(1)).toBe(true) - - func.reset() - func.throws('Error', 'intentional error') - expect(() => timer.time('test2', func, 1)).toThrow('intentional error') - expect(func.callCount).toBe(1) - }) - - test('start() and end()', async () => { - timer.start('test3') - await new Promise((resolve) => setTimeout(resolve, 10)) - timer.end('test3') - const summary = timer.summary - expect(summary.length).toBe(1) - const entry = summary[0] - expect(entry.name).toEqual('test3') - expect(entry.duration).toBeGreaterThanOrEqual(5) - expect(entry.duration).toBeLessThanOrEqual(30) - }) - - test('accessing operationId auto-increments the counter', () => { - expect(timer.operationId).toBe(1) - expect(timer.operationId).toBe(2) - }) -}) - describe('parseCacheControl', () => { test('accepts undefined', () => { const result = parseCacheControl() diff --git a/packages/pwa-kit-runtime/src/utils/ssr-server/performance-timer.js b/packages/pwa-kit-runtime/src/utils/ssr-server/performance-timer.js deleted file mode 100644 index 021a36e1b5..0000000000 --- a/packages/pwa-kit-runtime/src/utils/ssr-server/performance-timer.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2022, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import {PerformanceObserver, performance} from 'perf_hooks' - -/** - * Class that wraps the Node Performance API to guard against - * changes (it's at Stability 1 in node 8.10) and to make - * the usage simpler. - * - * To use: create an instance of this class, and then call start() and - * end(), passing the name of the duration being measured. To get all - * the measured values, use summary(). - * - * To time a function, use time(), passing a duration name - * (useful when the same function is called multiple times and you want - * to measure them separately) and function arguments. - * - * @private - */ -export class PerformanceTimer { - /** - * Construct a new PerformanceTimer, with the given name - * as a 'namespace' within which all durations can be - * measured. When the object is deleted, it clears all - * entries under this namespace, so multiple - * PerformanceTimer instances can be used at once. - * @private - * @param name - */ - constructor(name) { - this._namespace = `${name}-` - // Length of the namespace prefix (with the '-' postfix) - const nslen = this._namespace.length - - this._names = {} - const results = (this._results = []) - this._observer = new PerformanceObserver((list) => { - list.getEntries().forEach((entry) => { - const en = entry.name - // Only include PerformanceEntry objects in - // the namespace of this PerformanceTimer - if (en.startsWith(this._namespace)) { - results.push({ - name: en.slice(nslen), - duration: entry.duration - }) - } - }) - }, false) - this._observer.observe({entryTypes: ['measure']}) - this._nextOperationId = 1 - } - - /** - * Returns an operation id that's unique to this PerformanceTimer, - * so that timing code can use it to distinguish repeat timings. - * Returns a different value on each access. - * @return {number} - */ - get operationId() { - return this._nextOperationId++ - } - - /** - * Given a name, return a namespaced version of it, - * with the optional extension, and include teh result - * in the _names object. - * @private - */ - _getMarkName(name, extension) { - const ext = extension ? `-${extension}` : '' - const mark = `${this._namespace}${name}${ext}` - this._names[mark] = true - return mark - } - - /** - * Mark the start of the duration with the given name - * @private - * @param name {String} duration name - */ - start(name) { - performance.mark(this._getMarkName(name, 'start')) - } - - /** - * Mark the end of the duration with the given name - * @private - * @param name {String} duration name - */ - end(name) { - const startName = this._getMarkName(name, 'start') - const endName = this._getMarkName(name, 'end') - performance.mark(endName) - performance.measure(this._getMarkName(name), startName, endName) - performance.clearMarks(startName) - performance.clearMarks(endName) - } - - /** - * Clear the duration with the given name - * @private - * @param name {String} duration name - */ - clear(name) { - performance.clearMarks(this._getMarkName(name, 'start')) - performance.clearMarks(this._getMarkName(name, 'end')) - performance.clearMarks(this._getMarkName(name)) - } - - /** - * Finish with this PerformanceObserver - * @private - */ - finish() { - Object.keys(this._names).forEach((mark) => performance.clearMarks(mark)) - this._names = {} - this._observer.disconnect() - this._observer = null - } - - /** - * Measure the duration of the given function, - * which is called with any remaining arguments - * after name and fn. - * - * If the function throws an error, no duration - * is recorded. - * - * @private - * @param name {String} duration name - * @param fn {function} function to call - * @param args {Array} any arguments to the function - */ - time(name, fn, ...args) { - this.start(name) - try { - const result = fn(...args) - this.end(name) - return result - } catch (err) { - this.clear(name) - throw err - } - } - - /** - * Get an object (that can be JSON-serialized) for all - * measured times. - * @private - */ - get summary() { - return this._results - } - - /** - * Get access to the performance API used by this timer - * @private - * @return {Performance} - */ - get performance() { - /* istanbul ignore next */ - return performance - } -} diff --git a/packages/template-express-minimal/package.json b/packages/template-express-minimal/package.json index b0f23f0a89..ad72f3cb5e 100644 --- a/packages/template-express-minimal/package.json +++ b/packages/template-express-minimal/package.json @@ -22,7 +22,7 @@ "mobify": { "ssrEnabled": true, "ssrParameters": { - "ssrFunctionNodeVersion": "14.x" + "ssrFunctionNodeVersion": "16.x" }, "ssrOnly": [ "ssr.js", diff --git a/packages/template-retail-react-app/README.md b/packages/template-retail-react-app/README.md index 5f3ad1d072..1b230206eb 100644 --- a/packages/template-retail-react-app/README.md +++ b/packages/template-retail-react-app/README.md @@ -6,7 +6,7 @@ Developers don’t have to worry about the underlying infrastructure, whether th ## Requirements -- Node 14 +- Node 14.17.0 or later - npm 6.14.4 or later ## Get Started diff --git a/packages/template-retail-react-app/app/hoc/with-registration/index.test.js b/packages/template-retail-react-app/app/hoc/with-registration/index.test.js index 266e2a8a69..165cbdb328 100644 --- a/packages/template-retail-react-app/app/hoc/with-registration/index.test.js +++ b/packages/template-retail-react-app/app/hoc/with-registration/index.test.js @@ -50,12 +50,12 @@ beforeEach(() => { afterEach(() => { jest.resetModules() + sessionStorage.clear() }) test('should execute onClick for registered users', async () => { const onClick = jest.fn() - - renderWithProviders() + await renderWithProviders() await waitFor(() => { // we wait for login to complete and user's firstName to show up on screen. @@ -65,24 +65,29 @@ test('should execute onClick for registered users', async () => { const trigger = screen.getByText(/button/i) user.click(trigger) - expect(onClick).toHaveBeenCalledTimes(1) + await waitFor(() => { + expect(onClick).toHaveBeenCalledTimes(1) + }) }) -test('should show login modal if user not registered', () => { +test('should show login modal if user not registered', async () => { global.server.use( rest.get('*/customers/:customerId', (req, res, ctx) => { return res(ctx.delay(0), ctx.status(200), ctx.json(mockedGuestCustomer)) }) ) const onClick = jest.fn() + await renderWithProviders() - renderWithProviders() - - const trigger = screen.getByText(/button/i) - user.click(trigger) + const trigger = await screen.findByText(/button/i) + await waitFor(() => { + user.click(trigger) + }) - expect(screen.getByLabelText(/email/i)).toBeInTheDocument() - expect(screen.getByLabelText(/Password/)).toBeInTheDocument() - expect(screen.getByText(/forgot password/i)).toBeInTheDocument() - expect(screen.getByText(/sign in/i)).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByLabelText(/email/i)).toBeInTheDocument() + expect(screen.getByLabelText(/Password/)).toBeInTheDocument() + expect(screen.getByText(/forgot password/i)).toBeInTheDocument() + expect(screen.getByText(/sign in/i)).toBeInTheDocument() + }) }) diff --git a/packages/template-retail-react-app/app/hooks/use-product-view-modal.test.js b/packages/template-retail-react-app/app/hooks/use-product-view-modal.test.js index 2f68d5b946..6262b70982 100644 --- a/packages/template-retail-react-app/app/hooks/use-product-view-modal.test.js +++ b/packages/template-retail-react-app/app/hooks/use-product-view-modal.test.js @@ -8,14 +8,13 @@ import React from 'react' import {Router} from 'react-router-dom' import PropTypes from 'prop-types' -import {screen, render, fireEvent, waitFor} from '@testing-library/react' +import {screen, fireEvent, waitFor} from '@testing-library/react' import {createMemoryHistory} from 'history' import {IntlProvider} from 'react-intl' import mockProductDetail from '../commerce-api/mocks/variant-750518699578M' import {useProductViewModal} from './use-product-view-modal' -import {DEFAULT_LOCALE} from '../utils/test-utils' -import {renderWithProviders} from '../utils/test-utils' +import {DEFAULT_LOCALE, renderWithProviders} from '../utils/test-utils' import messages from '../translations/compiled/en-GB.json' import {rest} from 'msw' @@ -84,7 +83,7 @@ describe('useProductViewModal hook', () => { const history = createMemoryHistory() history.push('/test/path?color=BLACKFB') - render( + renderWithProviders( { const history = createMemoryHistory() history.push('/test/path') - render( + renderWithProviders( { const history = createMemoryHistory() history.push('/test/path') - render( + renderWithProviders( diff --git a/packages/template-retail-react-app/app/hooks/use-wishlist.test.js b/packages/template-retail-react-app/app/hooks/use-wishlist.test.js index 749eb15687..b903640ec6 100644 --- a/packages/template-retail-react-app/app/hooks/use-wishlist.test.js +++ b/packages/template-retail-react-app/app/hooks/use-wishlist.test.js @@ -316,13 +316,14 @@ describe('useWishlist hook', () => { test('createListItem also calls init if not already initialized', () => { const mock = jest.fn() const mockFindListByName = jest.fn().mockReturnValue({}) + const mockInit = jest.fn().mockReturnValue({id: 'test'}) useCustomerProductLists.mockReturnValue({ createListItem: mock, findListByName: mockFindListByName }) renderWithProviders() - result.init = jest.fn() + result.init = mockInit result.createListItem() diff --git a/packages/template-retail-react-app/app/pages/account/index.test.js b/packages/template-retail-react-app/app/pages/account/index.test.js index 8543ed1c2b..edb2ebe835 100644 --- a/packages/template-retail-react-app/app/pages/account/index.test.js +++ b/packages/template-retail-react-app/app/pages/account/index.test.js @@ -75,7 +75,7 @@ test('Provides navigation for subpages', async () => { return res(ctx.delay(0), ctx.json(mockOrderHistory)) }) ) - renderWithProviders(, { + await renderWithProviders(, { wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app} }) expect(await screen.findByTestId('account-page')).toBeInTheDocument() diff --git a/packages/template-retail-react-app/app/pages/account/orders.test.js b/packages/template-retail-react-app/app/pages/account/orders.test.js index 8e6b5dfd89..f6ef287a68 100644 --- a/packages/template-retail-react-app/app/pages/account/orders.test.js +++ b/packages/template-retail-react-app/app/pages/account/orders.test.js @@ -55,7 +55,7 @@ test('Renders order history and details', async () => { return res(ctx.delay(0), ctx.json(mockOrderProducts)) }) ) - renderWithProviders(, { + await renderWithProviders(, { wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app} }) expect(await screen.findByTestId('account-order-history-page')).toBeInTheDocument() diff --git a/packages/template-retail-react-app/app/pages/registration/index.test.jsx b/packages/template-retail-react-app/app/pages/registration/index.test.jsx index 404021de99..1416cbfa17 100644 --- a/packages/template-retail-react-app/app/pages/registration/index.test.jsx +++ b/packages/template-retail-react-app/app/pages/registration/index.test.jsx @@ -103,12 +103,12 @@ afterEach(() => { test('Allows customer to create an account', async () => { // render our test component - renderWithProviders(, { + await renderWithProviders(, { wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app} }) // fill out form and submit - const withinForm = within(screen.getByTestId('sf-auth-modal-form')) + const withinForm = within(await screen.findByTestId('sf-auth-modal-form')) user.paste(withinForm.getByLabelText('First Name'), 'Tester') user.paste(withinForm.getByLabelText('Last Name'), 'Tester') @@ -117,8 +117,8 @@ test('Allows customer to create an account', async () => { user.click(withinForm.getByText(/create account/i)) // wait for success state to appear + const myAccount = await screen.findAllByText(/My Account/) await waitFor(() => { - screen.logTestingPlaygroundURL() - expect(screen.getAllByText(/My Account/).length).toEqual(2) + expect(myAccount.length).toEqual(2) }) }) diff --git a/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx b/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx index 7c29ae0762..a71c8a42e4 100644 --- a/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx +++ b/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx @@ -64,11 +64,12 @@ afterEach(() => { test('Allows customer to go to sign in page', async () => { // render our test component - renderWithProviders(, { + await renderWithProviders(, { wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app} }) - user.click(screen.getByText('Sign in')) + user.click(await screen.findByText('Sign in')) + await waitFor(() => { expect(window.location.pathname).toEqual('/uk/en-GB/login') }) @@ -89,19 +90,24 @@ test('Allows customer to generate password token', async () => { ) ) // render our test component - renderWithProviders(, { + await renderWithProviders(, { wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app} }) // enter credentials and submit - user.type(screen.getByLabelText('Email'), 'foo@test.com') - user.click(within(screen.getByTestId('sf-auth-modal-form')).getByText(/reset password/i)) + user.type(await screen.findByLabelText('Email'), 'foo@test.com') + user.click(within(await screen.findByTestId('sf-auth-modal-form')).getByText(/reset password/i)) - // wait for success state expect(await screen.findByText(/password reset/i, {}, {timeout: 12000})).toBeInTheDocument() - expect(screen.getByText(/foo@test.com/i)).toBeInTheDocument() - user.click(screen.getByText('Back to Sign In')) + await waitFor(() => { + expect(screen.getByText(/foo@test.com/i)).toBeInTheDocument() + }) + + await waitFor(() => { + user.click(screen.getByText('Back to Sign In')) + }) + await waitFor(() => { expect(window.location.pathname).toEqual('/uk/en-GB/login') }) @@ -121,10 +127,10 @@ test('Renders error message from server', async () => { ) ) ) - renderWithProviders() + await renderWithProviders() - user.type(screen.getByLabelText('Email'), 'foo@test.com') - user.click(within(screen.getByTestId('sf-auth-modal-form')).getByText(/reset password/i)) + user.type(await screen.findByLabelText('Email'), 'foo@test.com') + user.click(within(await screen.findByTestId('sf-auth-modal-form')).getByText(/reset password/i)) expect(await screen.findByText('Something went wrong')).toBeInTheDocument() }) diff --git a/packages/template-retail-react-app/app/partials/product-view/index.test.js b/packages/template-retail-react-app/app/partials/product-view/index.test.js index 09b5d09ca5..18bf1a8f3c 100644 --- a/packages/template-retail-react-app/app/partials/product-view/index.test.js +++ b/packages/template-retail-react-app/app/partials/product-view/index.test.js @@ -53,11 +53,12 @@ beforeEach(() => { }) afterEach(() => { localStorage.clear() + sessionStorage.clear() }) -test('ProductView Component renders properly', () => { +test('ProductView Component renders properly', async () => { const addToCart = jest.fn() - renderWithProviders() + await renderWithProviders() expect(screen.getAllByText(/Black Single Pleat Athletic Fit Wool Suit/i).length).toEqual(2) expect(screen.getAllByText(/299.99/).length).toEqual(2) @@ -66,19 +67,24 @@ test('ProductView Component renders properly', () => { expect(screen.getAllByText(/add to cart/i).length).toEqual(2) }) -test('ProductView Component renders with addToCart event handler', () => { +test('ProductView Component renders with addToCart event handler', async () => { const addToCart = jest.fn() - renderWithProviders() + await renderWithProviders() const addToCartButton = screen.getAllByText(/add to cart/i)[0] fireEvent.click(addToCartButton) - expect(addToCart).toHaveBeenCalledTimes(1) + + await waitFor(() => { + expect(addToCart).toHaveBeenCalledTimes(1) + }) }) test('ProductView Component renders with addToWishList event handler', async () => { const addToWishlist = jest.fn() - renderWithProviders() + await renderWithProviders( + + ) await waitFor(() => { expect(screen.getByText(/customer: registered/)).toBeInTheDocument() @@ -95,7 +101,7 @@ test('ProductView Component renders with addToWishList event handler', async () test('ProductView Component renders with updateWishlist event handler', async () => { const updateWishlist = jest.fn() - renderWithProviders( + await renderWithProviders( ) @@ -111,12 +117,23 @@ test('ProductView Component renders with updateWishlist event handler', async () }) }) -test('Product View can update quantity', () => { +test('Product View can update quantity', async () => { const addToCart = jest.fn() - renderWithProviders() - const quantityBox = screen.getByRole('spinbutton') - expect(quantityBox).toHaveValue('1') + await renderWithProviders() + + let quantityBox + await waitFor(() => { + quantityBox = screen.getByRole('spinbutton') + }) + + await waitFor(() => { + expect(quantityBox).toHaveValue('1') + }) + // update item quantity userEvent.type(quantityBox, '{backspace}3') - expect(quantityBox).toHaveValue('3') + + await waitFor(() => { + expect(quantityBox).toHaveValue('3') + }) }) diff --git a/packages/template-retail-react-app/config/default.js b/packages/template-retail-react-app/config/default.js index ffdc9181c2..b9c0dd906a 100644 --- a/packages/template-retail-react-app/config/default.js +++ b/packages/template-retail-react-app/config/default.js @@ -48,7 +48,7 @@ module.exports = { '**/*.json' ], ssrParameters: { - ssrFunctionNodeVersion: '14.x', + ssrFunctionNodeVersion: '16.x', proxyConfigs: [ { host: 'kv7kzm78.api.commercecloud.salesforce.com', diff --git a/packages/template-retail-react-app/config/mocks/default.js b/packages/template-retail-react-app/config/mocks/default.js index 79c59f2c64..b35c27a1f1 100644 --- a/packages/template-retail-react-app/config/mocks/default.js +++ b/packages/template-retail-react-app/config/mocks/default.js @@ -107,7 +107,7 @@ module.exports = { ], // Additional parameters that configure Express app behavior. ssrParameters: { - ssrFunctionNodeVersion: '14.x', + ssrFunctionNodeVersion: '16.x', proxyConfigs: [ { host: 'localhost:8888', diff --git a/packages/template-retail-react-app/jest-setup.js b/packages/template-retail-react-app/jest-setup.js index e675917262..686886cb62 100644 --- a/packages/template-retail-react-app/jest-setup.js +++ b/packages/template-retail-react-app/jest-setup.js @@ -99,7 +99,7 @@ jest.mock('./app/commerce-api/utils', () => { global.TextEncoder = require('util').TextEncoder // This file consists of global mocks for jsdom. -class LocalStorageMock { +class StorageMock { constructor() { this.store = {} } @@ -117,14 +117,16 @@ class LocalStorageMock { } } -const localStorageMock = new LocalStorageMock() - Object.defineProperty(window, 'crypto', { value: new Crypto() }) Object.defineProperty(window, 'localStorage', { - value: localStorageMock + value: new StorageMock() +}) + +Object.defineProperty(window, 'sessionStorage', { + value: new StorageMock() }) Object.defineProperty(window, 'scrollTo', { diff --git a/packages/template-retail-react-app/package.json b/packages/template-retail-react-app/package.json index 7ed06e8240..879f955a7c 100644 --- a/packages/template-retail-react-app/package.json +++ b/packages/template-retail-react-app/package.json @@ -4,7 +4,7 @@ "license": "See license in LICENSE", "private": true, "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "devDependencies": { diff --git a/packages/template-typescript-minimal/package.json b/packages/template-typescript-minimal/package.json index 8f23ddeaf6..11ada7d9f9 100644 --- a/packages/template-typescript-minimal/package.json +++ b/packages/template-typescript-minimal/package.json @@ -2,7 +2,7 @@ "name": "typescript-minimal", "version": "2.7.0-dev", "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "private": true, @@ -48,7 +48,7 @@ "**/*.json" ], "ssrParameters": { - "ssrFunctionNodeVersion": "14.x", + "ssrFunctionNodeVersion": "16.x", "proxyConfigs": [ { "host": "kv7kzm78.api.commercecloud.salesforce.com", diff --git a/packages/test-commerce-sdk-react/package.json b/packages/test-commerce-sdk-react/package.json index bbbd58c018..fe1c5ac3b2 100644 --- a/packages/test-commerce-sdk-react/package.json +++ b/packages/test-commerce-sdk-react/package.json @@ -3,7 +3,7 @@ "version": "2.7.0-dev", "private": true, "engines": { - "node": "^14.0.0", + "node": "^14.0.0 || ^16.0.0", "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" }, "devDependencies": { @@ -49,7 +49,7 @@ "**/*.json" ], "ssrParameters": { - "ssrFunctionNodeVersion": "14.x", + "ssrFunctionNodeVersion": "16.x", "proxyConfigs": [ { "host": "kv7kzm78.api.commercecloud.salesforce.com",