diff --git a/packages/pwa-kit-create-app/scripts/build.js b/packages/pwa-kit-create-app/scripts/build.js index c4676e8f03..bd217ae00c 100644 --- a/packages/pwa-kit-create-app/scripts/build.js +++ b/packages/pwa-kit-create-app/scripts/build.js @@ -24,7 +24,8 @@ const main = () => { const pkgNames = [ 'template-retail-react-app', 'template-express-minimal', - 'template-typescript-minimal' + 'template-typescript-minimal', + 'template-mrt-reference-app' ] if (!sh.test('-d', templatesDir)) { diff --git a/packages/pwa-kit-create-app/scripts/create-mobify-app.js b/packages/pwa-kit-create-app/scripts/create-mobify-app.js index c4ff4a3f55..f9502bd4c3 100755 --- a/packages/pwa-kit-create-app/scripts/create-mobify-app.js +++ b/packages/pwa-kit-create-app/scripts/create-mobify-app.js @@ -60,11 +60,13 @@ const EXPRESS_MINIMAL = 'express-minimal' const TEST_PROJECT = 'test-project' // TODO: This will be replaced with the `isomorphic-client` config. const RETAIL_REACT_APP_DEMO = 'retail-react-app-demo' const RETAIL_REACT_APP = 'retail-react-app' +const MRT_REFERENCE_APP = 'mrt-reference-app' const PRIVATE_PRESETS = [ TEST_PROJECT, EXPRESS_MINIMAL_TEST_PROJECT, - TYPESCRIPT_MINIMAL_TEST_PROJECT + TYPESCRIPT_MINIMAL_TEST_PROJECT, + MRT_REFERENCE_APP ] const PUBLIC_PRESETS = [ RETAIL_REACT_APP_DEMO, @@ -464,6 +466,12 @@ const main = (opts) => { }) case TEST_PROJECT: return runGenerator(testProjectAnswers(), opts) + case MRT_REFERENCE_APP: + return runTemplateGenerator( + 'mrt-reference-app', + opts, + 'template-mrt-reference-app' + ) case RETAIL_REACT_APP_DEMO: return Promise.resolve() .then(() => runGenerator(demoProjectAnswers(), opts)) diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index 9e2036f55b..ace8dee897 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -366,12 +366,6 @@ const renderer = excludeWarnings: true, skipFirstNotification: true }), - - // Must only appear on one config – this one is the only mandatory one. - new CopyPlugin({ - patterns: [{from: 'app/static/', to: 'static/'}] - }), - analyzeBundle && getBundleAnalyzerPlugin('server-renderer') ].filter(Boolean) } @@ -395,6 +389,10 @@ const ssr = (() => { }, plugins: [ ...config.plugins, + // This must only appear on one config – this one is the only mandatory one. + new CopyPlugin({ + patterns: [{from: 'app/static/', to: 'static/'}] + }), analyzeBundle && getBundleAnalyzerPlugin(SSR) ].filter(Boolean) } diff --git a/packages/pwa-kit-dev/src/ssr/server/build-dev-server.js b/packages/pwa-kit-dev/src/ssr/server/build-dev-server.js index 7133287c69..149b0e2d9d 100644 --- a/packages/pwa-kit-dev/src/ssr/server/build-dev-server.js +++ b/packages/pwa-kit-dev/src/ssr/server/build-dev-server.js @@ -26,8 +26,6 @@ import { REQUEST_PROCESSOR } from '../../configs/webpack/config-names' import {randomUUID} from 'crypto' -const projectDir = process.cwd() -const projectWebpackPath = path.resolve(projectDir, 'webpack.config.js') const chalk = require('chalk') @@ -125,6 +123,8 @@ export const DevServerMixin = { // But the SSR render function must! let config = require('../../configs/webpack/config') + + const projectWebpackPath = path.resolve(app.options.projectDir, 'webpack.config.js') if (fs.existsSync(projectWebpackPath)) { config = require(projectWebpackPath) } @@ -214,36 +214,27 @@ export const DevServerMixin = { }, serveStaticFile(filePath, opts = {}) { + // Warning: Ugly part of the Bundle spec that we need to maintain. + // + // This function assumes that an SDK build step will copy all + // non-webpacked assets from the 'app' dir to the 'build' dir. + // + // If you look carefully through the history, this has never + // been true though – assets get copied from app/static to + // build/static but this isn't really clear from the API. + // + // To see where those assets get copied, see here: + // + // packages/pwa-kit-dev/src/configs/webpack/config.js + // + // We have plans to make a robust Bundle spec in 246! + // + // Discussion here: + // + // https://salesforce-internal.slack.com/archives/C8YDDMKFZ/p1677793769255659?thread_ts=1677791840.174309&cid=C8YDDMKFZ return (req, res) => { - req.app.__devMiddleware.waitUntilValid(() => { - const options = req.app.options - const webpackStats = req.app.__devMiddleware.context.stats.stats - - const serverCompilation = webpackStats.find( - // static files are copied into bundle - // in the server webpack config - (stat) => stat.compilation.name === SERVER - ).compilation - const {assetsInfo} = serverCompilation - const assetInfo = assetsInfo.get(filePath) - - // if the asset is not in the webpack bundle, then - // return 404, we don't care whether or not the file - // exists in the local file system - if (!assetInfo) { - res.sendStatus(404) - return - } - const {sourceFilename} = assetInfo - const sourceFilePath = path.resolve(sourceFilename) - - res.sendFile(sourceFilePath, { - headers: { - 'cache-control': options.defaultCacheControl - }, - ...opts - }) - }) + const baseDir = path.resolve(req.app.options.projectDir, 'app') + return this._serveStaticFile(req, res, baseDir, filePath, opts) } }, diff --git a/packages/pwa-kit-dev/src/ssr/server/build-dev-server.test.js b/packages/pwa-kit-dev/src/ssr/server/build-dev-server.test.js index 96b6ee0f10..e6f9e0a293 100644 --- a/packages/pwa-kit-dev/src/ssr/server/build-dev-server.test.js +++ b/packages/pwa-kit-dev/src/ssr/server/build-dev-server.test.js @@ -738,6 +738,7 @@ describe('DevServer service worker', () => { test(`${name} (and handle 404s correctly)`, () => { const app = createApp() + app.get('/worker.js(.map)?', NoWebpackDevServerFactory.serveServiceWorker) return request(app).get(requestPath).expect(404) }) @@ -745,52 +746,16 @@ describe('DevServer service worker', () => { }) describe('DevServer serveStaticFile', () => { - // This isn't ideal! We need a way to test the dev middleware - // including the on demand webpack compiler. However, the webpack config and - // the Dev server assumes the code runs at the root of a project. - // When we run the tests, we are not in a project. - // We have a /test_fixtures project, but Jest does not support process.chdir(), - // nor mocking process.cwd(), so we mock the dev middleware for now. - // TODO: create a proper testing fixture project and run the tests in the isolated - // project environment. - const MockWebpackDevMiddleware = { - waitUntilValid: (cb) => cb(), - context: { - stats: { - stats: [ - { - compilation: { - name: 'server', - assetsInfo: new Map([ - [ - 'static/favicon.ico', - { - sourceFilename: path.resolve( - testFixtures, - 'app/static/favicon.ico' - ) - } - ] - ]) - } - } - ] - } - } - } - test('should serve static files', async () => { - const options = opts() + const options = opts({projectDir: testFixtures}) const app = NoWebpackDevServerFactory._createApp(options) - app.__devMiddleware = MockWebpackDevMiddleware app.use('/test', NoWebpackDevServerFactory.serveStaticFile('static/favicon.ico')) return request(app).get('/test').expect(200) }) test('should return 404 if static file does not exist', async () => { - const options = opts() + const options = opts({projectDir: testFixtures}) const app = NoWebpackDevServerFactory._createApp(options) - app.__devMiddleware = MockWebpackDevMiddleware app.use('/test', NoWebpackDevServerFactory.serveStaticFile('static/IDoNotExist.ico')) return request(app).get('/test').expect(404) }) 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 91e33b2098..c9b3a87bbf 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 @@ -86,6 +86,9 @@ export const RemoteServerFactory = { * testing, or to handle non-standard projects. */ const defaults = { + // For test only – allow the project dir to be overridden. + projectDir: process.cwd(), + // Absolute path to the build directory buildDir: path.resolve(process.cwd(), BUILD), @@ -741,17 +744,25 @@ export const RemoteServerFactory = { */ serveStaticFile(filePath, opts = {}) { return (req, res) => { - const options = req.app.options - const file = path.resolve(options.buildDir, filePath) - res.sendFile(file, { - headers: { - [CACHE_CONTROL]: options.defaultCacheControl - }, - ...opts - }) + const baseDir = req.app.options.buildDir + return this._serveStaticFile(req, res, baseDir, filePath, opts) } }, + /** + * @private + */ + _serveStaticFile(req, res, baseDir, filePath, opts = {}) { + const options = req.app.options + const file = path.resolve(baseDir, filePath) + res.sendFile(file, { + headers: { + [CACHE_CONTROL]: options.defaultCacheControl + }, + ...opts + }) + }, + /** * Server side rendering entry. * diff --git a/packages/template-mrt-reference-app/.eslintignore b/packages/template-mrt-reference-app/.eslintignore new file mode 100644 index 0000000000..8fb1b26ee5 --- /dev/null +++ b/packages/template-mrt-reference-app/.eslintignore @@ -0,0 +1,7 @@ +build +coverage +docs +app/static +jest.config.js +webpack +scripts/generator/assets diff --git a/packages/template-mrt-reference-app/.eslintrc.js b/packages/template-mrt-reference-app/.eslintrc.js new file mode 100644 index 0000000000..39a63a98b9 --- /dev/null +++ b/packages/template-mrt-reference-app/.eslintrc.js @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2021, salesforce.com, 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 + */ +module.exports = { + extends: require.resolve('pwa-kit-dev/configs/eslint/eslint-config') +} diff --git a/packages/template-mrt-reference-app/.gitignore b/packages/template-mrt-reference-app/.gitignore new file mode 100644 index 0000000000..a6bff74fae --- /dev/null +++ b/packages/template-mrt-reference-app/.gitignore @@ -0,0 +1,3 @@ +build +node_modules +build.tar diff --git a/packages/template-mrt-reference-app/.prettierignore b/packages/template-mrt-reference-app/.prettierignore new file mode 100644 index 0000000000..312fde3744 --- /dev/null +++ b/packages/template-mrt-reference-app/.prettierignore @@ -0,0 +1,4 @@ +build +docs +coverage +scripts/generator/assets diff --git a/packages/template-mrt-reference-app/.prettierrc.yaml b/packages/template-mrt-reference-app/.prettierrc.yaml new file mode 100644 index 0000000000..33069bf2b2 --- /dev/null +++ b/packages/template-mrt-reference-app/.prettierrc.yaml @@ -0,0 +1,7 @@ +printWidth: 100 +singleQuote: true +semi: false +bracketSpacing: false +tabWidth: 4 +arrowParens: 'always' +trailingComma: 'none' diff --git a/packages/template-mrt-reference-app/LICENSE b/packages/template-mrt-reference-app/LICENSE new file mode 100644 index 0000000000..d8f701f7ed --- /dev/null +++ b/packages/template-mrt-reference-app/LICENSE @@ -0,0 +1,14 @@ +BSD 3-Clause License + +Copyright (c) 2021, Salesforce.com, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/template-mrt-reference-app/README.md b/packages/template-mrt-reference-app/README.md new file mode 100644 index 0000000000..c030329905 --- /dev/null +++ b/packages/template-mrt-reference-app/README.md @@ -0,0 +1,25 @@ +# template-mrt-reference-app + +This is the reference app that the Managed Runtime Team uses to test +features in the platform (eg. TLS versions, successful deploys, proxy +behaviour). + +This app is intended to be a thin layer over the bare minimum SDKs +that we expect/require all MRT users to use. + +Although MRT started life primarily as a hosting environment for +React apps, we're expanding that to support other technologies – +this app lets us test those platform features that are universal +across all apps, regardless of framework choice. + + +## Usage in CI/CD tests ⛅️ + +This app is deployed to several pre-existing test Targets as part +of a "smoke-test" of the MRT platform. To see the Targets in use +take a look at the CI config in + +https://git.soma.salesforce.com/cc-mobify/ssr-infrastructure/blob/sfci-main/Jenkinsfile#L176 + +These smoke-tests are triggered by merges to the main development +branch of the above repository. diff --git a/packages/template-mrt-reference-app/app/request-processor.js b/packages/template-mrt-reference-app/app/request-processor.js new file mode 100644 index 0000000000..f0b2505d57 --- /dev/null +++ b/packages/template-mrt-reference-app/app/request-processor.js @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, 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 {QueryParameters} from 'pwa-kit-runtime/utils/ssr-request-processing' + +const exclusions = ['removeme'] + +export const processRequest = ({path, querystring, parameters}) => { + console.assert(parameters, 'Missing parameters') + + // Query string filtering + + // Build a first QueryParameters object from the given querystring + const queryParameters = new QueryParameters(querystring) + + // Build a second QueryParameters from the first, with all + // excluded parameters removed + const filtered = QueryParameters.from( + queryParameters.parameters.filter( + // parameter.key is always lower-case + (parameter) => !exclusions.includes(parameter.key) + ) + ) + + // Re-generate the querystring + querystring = filtered.toString() + + return { + path, + querystring + } +} diff --git a/packages/template-mrt-reference-app/app/ssr.js b/packages/template-mrt-reference-app/app/ssr.js new file mode 100644 index 0000000000..a741e9753e --- /dev/null +++ b/packages/template-mrt-reference-app/app/ssr.js @@ -0,0 +1,246 @@ +/* + * 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 + */ + +/** + * Notes on this test app 🧠 + * + * HTTP requests to **all** paths except those listed below will return + * a JSON response containing useful diagnostic values from the request + * and the context in which the request is handled. Values are **whitelisted**, + * so if you want to view a new header or environment variable you will need + * to add it to the appropriate whitelist. Do **NOT** expose any values that + * contain potentially sensitive information (such as API keys or AWS + * credentials), especially from the environment. The deployed server is + * globally accessible. + * + * - `/exception`: Throws a custom error whose textual representation (visible in the HTTP response) is the same diagnostic information described above. + * - `/cache`: Returns the same diagnostic data, but will store it (as text) in an S3 object in the application cache, then retrieve it and return it. This tests access to the application cache. + * - `/auth/`: Requires HTTP basic authentication with the username `mobify` and the password `supersecret` + * - `/auth/logout`: Returns a 401 response that will remove any existing authentication data for a target + * + * The app will normally use the 'context.succeed' callback to return a + * response to the Lambda integration code. If the query parameter `directcallback` + * is set to any non-empty value, it will use the callback passed to the Lambda + * entry point instead. This allows testing of different SDK or code methods + * of generating responses. + * + * A `Cache-Control: no-cache` header is added to **all** responses, so CloudFront + * will never cache any of the responses from this test server. You therefore + * don't need to add cachebreakers when running tests. + * + * The server has a proxy configured to [HTTPBin](https://httpbin.org/). To send + * a test request to it, use the path `/mobify/proxy/httpbin/` - for example, + * `/mobify/proxy/httpbin/get` + * + * A test bundle file is available at `/mobify/bundle//assets/mobify.png` + * where BUNDLE_NUMBER is the most recently published bundle number. + */ + +const path = require('path') +const {getRuntime} = require('pwa-kit-runtime/ssr/server/express') +const pkg = require('../package.json') +const basicAuth = require('express-basic-auth') +const fetch = require('cross-fetch') + +/** + * Custom error class + */ +class IntentionalError extends Error { + constructor(diagnostics, ...params) { + super(...params) + this.message = JSON.stringify(diagnostics, null, 2) + this.name = 'IntentionalError' + } +} + +const ENVS_TO_EXPOSE = [ + 'aws_execution_env', + 'aws_lambda_function_memory_size', + 'aws_lambda_function_name', + 'aws_lambda_function_version', + 'aws_lambda_log_group_name', + 'aws_lambda_log_stream_name', + 'aws_region', + 'bundle_id', + 'deploy_id', + 'deploy_target', + 'external_domain_name', + 'mobify_property_id', + 'node_env', + 'tz' +] + +const HEADERS_TO_REDACT = ['x-api-key', 'x-apigateway-context', 'x-apigateway-event'] + +const BADSSL_TLS1_1_URL = 'https://tls-v1-1.badssl.com:1011/' +const BADSSL_TLS1_2_URL = 'https://tls-v1-2.badssl.com:1012/' + +const redactAndSortObjectKeys = (o, redactList = HEADERS_TO_REDACT) => { + const redact = (k) => ({[k]: redactList.includes(k) ? '*****' : o[k]}) + return Object.assign({}, ...Object.keys(o).sort().map(redact)) +} + +/** + * Shallow-clone the given object such that the only keys on the + * clone are those in the given whitelist, and so that the keys are + * in alphanumeric sort order. + * @param o the object to clone + * @param whitelist an Array of strings for keys that should be included. + * If a string ends in a '*', the key may contain zero or more characters + * matched by the '*' (i.e., it must start with the whitelist string up to + * but not including the '*') + * @return {{}} + */ +const filterAndSortObjectKeys = (o, whitelist) => + o && + Object.keys(o) + // Include only whitelisted keys + .filter((key) => { + const keylc = key.toLowerCase().trim() + return whitelist.some( + (pattern) => + // wildcard matching + (pattern.endsWith('*') && keylc.startsWith(pattern.slice(0, -1))) || + pattern === keylc // equality matching + ) + }) + // Sort the remaining keys + .sort() + // Include values + .reduce((acc, key) => { + acc[key] = o[key] + return acc + }, {}) + +/** + * Return a JSON-serializable object with key diagnostic values from a request + */ +const jsonFromRequest = (req) => { + return { + protocol: req.protocol, + method: req.method, + path: req.path, + query: req.query, + route_path: req.route.path, + body: req.body, + headers: redactAndSortObjectKeys(req.headers), + ip: req.ip, + env: filterAndSortObjectKeys(process.env, ENVS_TO_EXPOSE) + } +} + +/** + * Express handler that returns a JSON response with diagnostic values + */ +const echo = (req, res) => res.json(jsonFromRequest(req)) + +/** + * Express handler that throws an IntentionalError + */ +const exception = (req) => { + // Intentionally throw an exception so that we can check for it + // in logs. + throw new IntentionalError(jsonFromRequest(req)) +} + +/** + * Express handler that makes 2 requests to badssl TLS testing domains + * to verify that our applications can only make requests to domains with + * updated TLS versions. + */ +const tlsVersionTest = async (_, res) => { + let response11 = await fetch(BADSSL_TLS1_1_URL) + .then((res) => res.ok) + .catch(() => false) + let response12 = await fetch(BADSSL_TLS1_2_URL) + .then((res) => res.ok) + .catch(() => false) + res.header('Content-Type', 'application/json') + res.send(JSON.stringify({'tls1.1': response11, 'tls1.2': response12}, null, 4)) +} + +/** + * Logging middleware; logs request and response headers (and response status). + */ +const loggingMiddleware = (req, res, next) => { + // Log request headers + console.log(`Request: ${req.method} ${req.originalUrl}`) + console.log(`Request headers: ${JSON.stringify(req.headers, null, 2)}`) + // Arrange to log response status and headers + res.on('finish', () => { + const statusCode = res._header ? String(res.statusCode) : String(-1) + console.log(`Response status: ${statusCode}`) + if (res.headersSent) { + const headers = JSON.stringify(res.getHeaders(), null, 2) + console.log(`Response headers: ${headers}`) + } + }) + + return next() +} + +const options = { + // The build directory (an absolute path) + buildDir: path.resolve(process.cwd(), 'build'), + + // The cache time for SSR'd pages (defaults to 600 seconds) + defaultCacheTimeSeconds: 600, + + // The port that the local dev server listens on + port: 3000, + + // The protocol on which the development Express app listens. + // Note that http://localhost is treated as a secure context for development. + protocol: 'http', + + mobify: pkg.mobify +} + +const runtime = getRuntime() + +const {handler, app, server} = runtime.createHandler(options, (app) => { + app.get('/favicon.ico', runtime.serveStaticFile('static/favicon.ico')) + + // Add middleware to explicitly suppress caching on all responses (done + // before we invoke the handlers) + app.use((req, res, next) => { + res.set('Cache-Control', 'no-cache') + return next() + }) + + // Add middleware to log request and response headers + app.use(loggingMiddleware) + + // Configure routes + app.all('/exception', exception) + app.get('/tls', tlsVersionTest) + + // Add a /auth/logout path that will always send a 401 (to allow clearing + // of browser credentials) + app.all('/auth/logout', (req, res) => res.status(401).send('Logged out')) + // Add auth middleware to the /auth paths only + app.use( + '/auth*', + basicAuth({ + users: {mobify: 'supersecret'}, + challenge: true, + // Use a realm that is different per target + realm: process.env.EXTERNAL_DOMAIN_NAME || 'echo-test' + }) + ) + app.all('/auth*', echo) + // All other paths/routes invoke echo directly + app.all('/*', echo) + app.set('json spaces', 4) +}) + +// SSR requires that we export a single handler function called 'get', that +// supports AWS use of the server that we created above. +exports.get = handler +exports.server = server + +exports.app = app diff --git a/packages/template-mrt-reference-app/app/ssr.test.js b/packages/template-mrt-reference-app/app/ssr.test.js new file mode 100644 index 0000000000..864dfedd4a --- /dev/null +++ b/packages/template-mrt-reference-app/app/ssr.test.js @@ -0,0 +1,39 @@ +/* + * 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 + */ +const request = require('supertest') + +describe('server', () => { + let originalEnv, app, server + beforeEach(() => { + originalEnv = process.env + process.env = Object.assign({}, process.env, { + LISTEN_ADDRESS: '', + BUNDLE_ID: '1', + DEPLOY_TARGET: 'test', + EXTERNAL_DOMAIN_NAME: 'test.com', + MOBIFY_PROPERTY_ID: 'test', + AWS_LAMBDA_FUNCTION_NAME: 'pretend-to-be-remote' + }) + const ssr = require('./ssr') + app = ssr.app + server = ssr.server + }) + afterEach(() => { + process.env = originalEnv + server.close() + }) + test.each([ + ['/', 200, 'application/json; charset=utf-8'], + ['/tls', 200, 'application/json; charset=utf-8'], + ['/exception', 500, 'text/html; charset=utf-8'] + ])('Path %p should render correctly', (path, expectedStatus, expectedContentType) => { + return request(app) + .get(path) + .expect(expectedStatus) + .expect('Content-Type', expectedContentType) + }) +}) diff --git a/packages/template-mrt-reference-app/app/static/favicon.ico b/packages/template-mrt-reference-app/app/static/favicon.ico new file mode 100644 index 0000000000..a15a8f348b Binary files /dev/null and b/packages/template-mrt-reference-app/app/static/favicon.ico differ diff --git a/packages/template-mrt-reference-app/babel.config.js b/packages/template-mrt-reference-app/babel.config.js new file mode 100644 index 0000000000..14a9594396 --- /dev/null +++ b/packages/template-mrt-reference-app/babel.config.js @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2021, salesforce.com, 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 + */ +const base = require('pwa-kit-dev/configs/babel/babel-config').default + +module.exports = { + ...base, + exclude: ['./app/ssr.js', './app/ssr.test.js'] +} diff --git a/packages/template-mrt-reference-app/jest.config.js b/packages/template-mrt-reference-app/jest.config.js new file mode 100644 index 0000000000..5671b7a86b --- /dev/null +++ b/packages/template-mrt-reference-app/jest.config.js @@ -0,0 +1,20 @@ +/* + * 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 + */ +const base = require('pwa-kit-dev/configs/jest/jest.config.js') + +module.exports = { + ...base, + coverageThreshold: { + global: { + branches: 64, + functions: 77, + lines: 85, + statements: 85 + } + }, + collectCoverageFrom: ['app/**'] +} diff --git a/packages/template-mrt-reference-app/package-lock.json b/packages/template-mrt-reference-app/package-lock.json new file mode 100644 index 0000000000..c72b8dcf7c --- /dev/null +++ b/packages/template-mrt-reference-app/package-lock.json @@ -0,0 +1,363 @@ +{ + "name": "template-mrt-reference-app", + "version": "2.7.0-dev", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@loadable/component": { + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.3.tgz", + "integrity": "sha512-VOgYgCABn6+/7aGIpg7m0Ruj34tGetaJzt4bQ345FwEovDQZ+dua+NWLmuJKv8rWZyxOUSfoJkmGnzyDXH2BAQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.7", + "hoist-non-react-statics": "^3.3.1", + "react-is": "^16.12.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "cookiejar": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dev": true, + "requires": { + "basic-auth": "^2.0.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "supertest": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", + "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/packages/template-mrt-reference-app/package.json b/packages/template-mrt-reference-app/package.json new file mode 100644 index 0000000000..979e7c6ffe --- /dev/null +++ b/packages/template-mrt-reference-app/package.json @@ -0,0 +1,48 @@ +{ + "name": "template-mrt-reference-app", + "version": "2.8.0-dev", + "engines": { + "node": "^14.0.0 || ^16.0.0", + "npm": "^6.14.4 || ^7.0.0 || ^8.0.0" + }, + "license": "See license in LICENSE", + "private": true, + "devDependencies": { + "pwa-kit-dev": "^2.8.0-dev", + "pwa-kit-runtime": "^2.8.0-dev", + "supertest": "^4.0.2", + "cross-fetch": "^3.1.4", + "express-basic-auth": "^1.2.0", + "@loadable/component": "^5.15.0" + }, + "scripts": { + "test": "pwa-kit-dev test", + "format": "pwa-kit-dev format \"**/*.{js,jsx}\"", + "lint": "pwa-kit-dev lint \"**/*.{js,jsx}\"", + "lint:fix": "npm run lint -- --fix", + "start": "pwa-kit-dev start", + "build": "pwa-kit-dev build", + "push": "npm run build && pwa-kit-dev push", + "save-credentials": "pwa-kit-dev save-credentials", + "tail-logs": "pwa-kit-dev tail-logs" + }, + "mobify": { + "ssrEnabled": true, + "ssrParameters": { + "ssrFunctionNodeVersion": "16.x", + "proxyConfigs": [ + { + "host": "httpbin.org", + "path": "httpbin" + } + ] + }, + "ssrOnly": [ + "ssr.js", + "node_modules/**/*.*" + ], + "ssrShared": [ + "intentionally-does-not-exist" + ] + } +}