From 30085fe8ffa40b57b99c7ab8c22761d491fed0dd Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 14 Mar 2022 14:27:58 -0400 Subject: [PATCH 1/7] Extract dedicated polyfill package **Why**: - So that a majority of users don't have to pay a performance penalty associated with supporting legacy browsers. - Simplify logic for initialization by setting expectation of feature availability - And, consequently, as an incremental step toward dropping polyfill/IE11 support altogether - Reduce size of bundles by excluding more polyfills injected by core-js (notably, Promise and URL polyfills) --- app/components/clipboard_button_component.js | 6 +- app/components/phone_input_component.js | 6 +- app/components/time_component.js | 6 +- app/components/validated_field_component.js | 6 +- app/helpers/script_helper.rb | 9 ++- .../packages/clipboard-button/index.spec.js | 7 +- app/javascript/packages/polyfill/index.js | 73 ++---------------- app/javascript/packages/polyfill/is-safe.js | 17 ----- .../packages/polyfill/is-safe.spec.js | 17 ----- app/javascript/packages/polyfill/package.json | 1 + app/javascript/packs/doc-capture-polling.js | 21 +++-- app/javascript/packs/document-capture.jsx | 5 +- app/javascript/packs/form-steps-wait.jsx | 7 +- app/javascript/packs/form-validation.js | 10 +-- app/javascript/packs/one-time-code-input.js | 3 +- app/javascript/packs/polyfill.ts | 1 + app/javascript/packs/webauthn-setup.js | 76 +++++++++---------- app/javascript/packs/webauthn-unhide.js | 9 +-- babel.config.js | 2 +- webpack.config.js | 2 +- yarn.lock | 5 ++ 21 files changed, 89 insertions(+), 200 deletions(-) delete mode 100644 app/javascript/packages/polyfill/is-safe.js delete mode 100644 app/javascript/packages/polyfill/is-safe.spec.js create mode 100644 app/javascript/packs/polyfill.ts diff --git a/app/components/clipboard_button_component.js b/app/components/clipboard_button_component.js index 1a05087c2bb..97b93a9b06d 100644 --- a/app/components/clipboard_button_component.js +++ b/app/components/clipboard_button_component.js @@ -1,5 +1,3 @@ -import { loadPolyfills } from '@18f/identity-polyfill'; +import { ClipboardButton } from '@18f/identity-clipboard-button'; -loadPolyfills(['custom-elements', 'clipboard']) - .then(() => import('@18f/identity-clipboard-button')) - .then(({ ClipboardButton }) => customElements.define('lg-clipboard-button', ClipboardButton)); +customElements.define('lg-clipboard-button', ClipboardButton); diff --git a/app/components/phone_input_component.js b/app/components/phone_input_component.js index 32b5c564228..ea7be1b7b79 100644 --- a/app/components/phone_input_component.js +++ b/app/components/phone_input_component.js @@ -1,5 +1,3 @@ -import { loadPolyfills } from '@18f/identity-polyfill'; +import { PhoneInput } from '@18f/identity-phone-input'; -loadPolyfills(['custom-elements', 'classlist', 'custom-event']) - .then(() => import('@18f/identity-phone-input')) - .then(({ PhoneInput }) => customElements.define('lg-phone-input', PhoneInput)); +customElements.define('lg-phone-input', PhoneInput); diff --git a/app/components/time_component.js b/app/components/time_component.js index e071164bc00..d85528fe09d 100644 --- a/app/components/time_component.js +++ b/app/components/time_component.js @@ -1,5 +1,3 @@ -import { loadPolyfills } from '@18f/identity-polyfill'; +import { TimeElement } from '@18f/identity-time-element'; -loadPolyfills(['custom-elements']) - .then(() => import('@18f/identity-time-element')) - .then(({ TimeElement }) => customElements.define('lg-time', TimeElement)); +customElements.define('lg-time', TimeElement); diff --git a/app/components/validated_field_component.js b/app/components/validated_field_component.js index c235c0826a1..ce33f96a7f4 100644 --- a/app/components/validated_field_component.js +++ b/app/components/validated_field_component.js @@ -1,5 +1,3 @@ -import { loadPolyfills } from '@18f/identity-polyfill'; +import { ValidatedField } from '@18f/identity-validated-field'; -loadPolyfills(['custom-elements', 'classlist']) - .then(() => import('@18f/identity-validated-field')) - .then(({ ValidatedField }) => customElements.define('lg-validated-field', ValidatedField)); +customElements.define('lg-validated-field', ValidatedField); diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb index 5626a316d91..7eb47ef47b9 100644 --- a/app/helpers/script_helper.rb +++ b/app/helpers/script_helper.rb @@ -22,7 +22,14 @@ def javascript_packs_tag_once(*names, prepend: false) def render_javascript_pack_once_tags(*names) javascript_packs_tag_once(*names) if names.present? - javascript_include_tag(*AssetSources.get_sources(*@scripts)) if @scripts + if @scripts.present? + safe_join( + [ + javascript_include_tag(*AssetSources.get_sources('polyfill'), nomodule: ''), + javascript_include_tag(*AssetSources.get_sources(*@scripts)), + ], + ) + end end end # rubocop:enable Rails/HelperInstanceVariable diff --git a/app/javascript/packages/clipboard-button/index.spec.js b/app/javascript/packages/clipboard-button/index.spec.js index 362333dadd4..ce6507ac8a1 100644 --- a/app/javascript/packages/clipboard-button/index.spec.js +++ b/app/javascript/packages/clipboard-button/index.spec.js @@ -1,14 +1,11 @@ +import 'clipboard-polyfill/overwrite-globals'; // See: https://github.com/jsdom/jsdom/issues/1568 import sinon from 'sinon'; import { getByRole } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; -import { loadPolyfills } from '@18f/identity-polyfill'; import { ClipboardButton } from './index.js'; describe('ClipboardButton', () => { - before(async () => { - // Necessary until: https://github.com/jsdom/jsdom/issues/1568 - await loadPolyfills(['clipboard']); - + before(() => { if (!customElements.get('lg-clipboard-button')) { customElements.define('lg-clipboard-button', ClipboardButton); } diff --git a/app/javascript/packages/polyfill/index.js b/app/javascript/packages/polyfill/index.js index 6e5c76a1d3a..cc7135ba033 100644 --- a/app/javascript/packages/polyfill/index.js +++ b/app/javascript/packages/polyfill/index.js @@ -1,65 +1,8 @@ -import isSafe from './is-safe'; - -/** - * @typedef Polyfill - * - * @prop {()=>boolean} test Test function, returning true if feature is detected as supported. - * @prop {()=>Promise} load Function to load polyfill module. - */ - -/** - * @typedef {"fetch"|"classlist"|"clipboard"|"crypto"|"custom-elements"|"custom-event"|"url"} SupportedPolyfills - */ - -/** - * @type {Record} - */ -const POLYFILLS = { - fetch: { - test: () => 'fetch' in window, - load: () => import(/* webpackChunkName: "whatwg-fetch" */ 'whatwg-fetch'), - }, - classlist: { - test: () => 'classList' in Element.prototype, - load: () => import(/* webpackChunkName: "classlist-polyfill" */ 'classlist-polyfill'), - }, - clipboard: { - test: () => 'clipboard' in navigator, - load: () => - import(/* webpackChunkName: "clipboard-polyfill" */ 'clipboard-polyfill/overwrite-globals'), - }, - crypto: { - test: () => 'crypto' in window, - load: () => import(/* webpackChunkName: "webcrypto-shim" */ 'webcrypto-shim'), - }, - 'custom-elements': { - test: () => 'customElements' in window, - load: () => - import(/* webpackChunkName: "custom-elements-polyfill" */ '@webcomponents/custom-elements'), - }, - 'custom-event': { - test: () => isSafe(() => new window.CustomEvent('test')), - load: () => import(/* webpackChunkName: "custom-event-polyfill" */ 'custom-event-polyfill'), - }, - url: { - test: () => isSafe(() => new URL('http://example.com')) && isSafe(() => new URLSearchParams()), - load: () => import(/* webpackChunkName: "js-polyfills-url" */ 'js-polyfills/url'), - }, -}; - -/** - * Given an array of supported polyfill names, loads polyfill if necessary. Returns a promise which - * resolves once all have been loaded. - * - * @param {SupportedPolyfills[]} polyfills Names of polyfills to load, if necessary. - * - * @return {Promise} - */ -export function loadPolyfills(polyfills) { - return Promise.all( - polyfills.map((name) => { - const { test, load } = POLYFILLS[name]; - return test() ? Promise.resolve() : load(); - }), - ); -} +import 'promise-polyfill/src/polyfill'; +import 'whatwg-fetch'; +import 'classlist-polyfill'; +import 'clipboard-polyfill/overwrite-globals'; +import 'webcrypto-shim'; +import '@webcomponents/custom-elements'; +import 'custom-event-polyfill'; +import 'js-polyfills/url'; diff --git a/app/javascript/packages/polyfill/is-safe.js b/app/javascript/packages/polyfill/is-safe.js deleted file mode 100644 index 002df2bd6b9..00000000000 --- a/app/javascript/packages/polyfill/is-safe.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Returns true if an invocation of the given function does not throw an error, or false otherwise. - * - * @param {() => any} fn Function to invoke. - * - * @return {boolean} - */ -function isSafe(fn) { - try { - fn(); - return true; - } catch { - return false; - } -} - -export default isSafe; diff --git a/app/javascript/packages/polyfill/is-safe.spec.js b/app/javascript/packages/polyfill/is-safe.spec.js deleted file mode 100644 index 4384e514d46..00000000000 --- a/app/javascript/packages/polyfill/is-safe.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import isSafe from './is-safe'; - -describe('isSafe', () => { - it('returns true if no error is thrown in invocation', () => { - const result = isSafe(() => ''); - - expect(result).to.equal(true); - }); - - it('returns false if error is thrown in invocation', () => { - const result = isSafe(() => { - throw new Error(); - }); - - expect(result).to.be.false(); - }); -}); diff --git a/app/javascript/packages/polyfill/package.json b/app/javascript/packages/polyfill/package.json index 2f7f9ebd8c4..17eb3423eb6 100644 --- a/app/javascript/packages/polyfill/package.json +++ b/app/javascript/packages/polyfill/package.json @@ -8,6 +8,7 @@ "clipboard-polyfill": "^3.0.3", "custom-event-polyfill": "^1.0.7", "js-polyfills": "^0.1.43", + "promise-polyfill": "^8.2.3", "webcrypto-shim": "^0.1.7", "whatwg-fetch": "^3.4.0" } diff --git a/app/javascript/packs/doc-capture-polling.js b/app/javascript/packs/doc-capture-polling.js index dad6e72ad67..d5793c4bed1 100644 --- a/app/javascript/packs/doc-capture-polling.js +++ b/app/javascript/packs/doc-capture-polling.js @@ -1,15 +1,12 @@ -import { loadPolyfills } from '@18f/identity-polyfill'; import { DocumentCapturePolling } from '@18f/identity-document-capture-polling'; import { getPageData } from '@18f/identity-page-data'; -loadPolyfills(['fetch', 'classlist']).then(() => { - new DocumentCapturePolling({ - statusEndpoint: /** @type {string} */ (getPageData('docCaptureStatusEndpoint')), - elements: { - backLink: /** @type {HTMLAnchorElement} */ (document.querySelector('.link-sent-back-link')), - form: /** @type {HTMLFormElement} */ (document.querySelector( - '.link-sent-continue-button-form', - )), - }, - }).bind(); -}); +new DocumentCapturePolling({ + statusEndpoint: /** @type {string} */ (getPageData('docCaptureStatusEndpoint')), + elements: { + backLink: /** @type {HTMLAnchorElement} */ (document.querySelector('.link-sent-back-link')), + form: /** @type {HTMLFormElement} */ (document.querySelector( + '.link-sent-continue-button-form', + )), + }, +}).bind(); diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index a9c4c2986c2..0475652fa49 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -13,7 +13,6 @@ import { HelpCenterContextProvider, } from '@18f/identity-document-capture'; import { i18n } from '@18f/identity-i18n'; -import { loadPolyfills } from '@18f/identity-polyfill'; import { isCameraCapableMobile } from '@18f/identity-device'; import { trackEvent } from '@18f/identity-analytics'; import { I18nContext } from '@18f/identity-react-i18n'; @@ -125,7 +124,7 @@ function addPageAction(action) { const noticeError = (error) => /** @type {DocumentCaptureGlobal} */ (window).newrelic?.noticeError(error); -loadPolyfills(['fetch', 'crypto', 'url']).then(async () => { +(async () => { const backgroundUploadURLs = getBackgroundUploadURLs(); const isAsyncForm = Object.keys(backgroundUploadURLs).length > 0; const csrf = getMetaContent('csrf-token'); @@ -211,4 +210,4 @@ loadPolyfills(['fetch', 'crypto', 'url']).then(async () => { ); render(, appRoot); -}); +})(); diff --git a/app/javascript/packs/form-steps-wait.jsx b/app/javascript/packs/form-steps-wait.jsx index 324bc6e238d..918a72219f9 100644 --- a/app/javascript/packs/form-steps-wait.jsx +++ b/app/javascript/packs/form-steps-wait.jsx @@ -1,6 +1,5 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Alert } from '@18f/identity-components'; -import { loadPolyfills } from '@18f/identity-polyfill'; /** * @typedef FormStepsWaitElements @@ -188,7 +187,5 @@ export class FormStepsWait { } } -loadPolyfills(['fetch', 'custom-event']).then(() => { - const forms = Array.from(document.querySelectorAll('[data-form-steps-wait]')); - forms.forEach((form) => new FormStepsWait(form).bind()); -}); +const forms = Array.from(document.querySelectorAll('[data-form-steps-wait]')); +forms.forEach((form) => new FormStepsWait(form).bind()); diff --git a/app/javascript/packs/form-validation.js b/app/javascript/packs/form-validation.js index 34f1a0520fd..43ee0915c49 100644 --- a/app/javascript/packs/form-validation.js +++ b/app/javascript/packs/form-validation.js @@ -1,5 +1,4 @@ import { t } from '@18f/identity-i18n'; -import { loadPolyfills } from '@18f/identity-polyfill'; /** * Given a submit event, disables all submit buttons within the target form. @@ -78,9 +77,6 @@ export function initialize(form) { form.addEventListener('submit', disableFormSubmit); } -loadPolyfills(['classlist']).then(() => { - /** @type {HTMLFormElement[]} */ - const forms = Array.from(document.querySelectorAll('form[data-validate]')); - - forms.forEach(initialize); -}); +/** @type {HTMLFormElement[]} */ +const forms = Array.from(document.querySelectorAll('form[data-validate]')); +forms.forEach(initialize); diff --git a/app/javascript/packs/one-time-code-input.js b/app/javascript/packs/one-time-code-input.js index 63a36a91f0b..fba60a12bf6 100644 --- a/app/javascript/packs/one-time-code-input.js +++ b/app/javascript/packs/one-time-code-input.js @@ -1,8 +1,7 @@ import OneTimeCodeInput from '@18f/identity-one-time-code-input'; -import { loadPolyfills } from '@18f/identity-polyfill'; const fakeField = /** @type {HTMLInputElement?} */ (document.querySelector('.one-time-code-input')); if (fakeField) { - loadPolyfills(['custom-event']).then(() => new OneTimeCodeInput(fakeField).bind()); + new OneTimeCodeInput(fakeField).bind(); } diff --git a/app/javascript/packs/polyfill.ts b/app/javascript/packs/polyfill.ts new file mode 100644 index 00000000000..42c8558ad72 --- /dev/null +++ b/app/javascript/packs/polyfill.ts @@ -0,0 +1 @@ +import '@18f/identity-polyfill'; diff --git a/app/javascript/packs/webauthn-setup.js b/app/javascript/packs/webauthn-setup.js index f574bd7f0e2..5ec9037287f 100644 --- a/app/javascript/packs/webauthn-setup.js +++ b/app/javascript/packs/webauthn-setup.js @@ -1,4 +1,3 @@ -import { loadPolyfills } from '@18f/identity-polyfill'; import { isWebAuthnEnabled, enrollWebauthnDevice } from '../app/webauthn'; /** @@ -16,47 +15,42 @@ export function reloadWithError(error, { force = false } = {}) { } } -function webauthn() { - if (!isWebAuthnEnabled()) { - reloadWithError('NotSupportedError'); - } - const continueButton = document.getElementById('continue-button'); - continueButton.addEventListener('click', () => { - document.getElementById('spinner').classList.remove('hidden'); - document.getElementById('continue-button').className = 'hidden'; +if (!isWebAuthnEnabled()) { + reloadWithError('NotSupportedError'); +} +const continueButton = document.getElementById('continue-button'); +continueButton.addEventListener('click', () => { + document.getElementById('spinner').classList.remove('hidden'); + document.getElementById('continue-button').className = 'hidden'; - const platformAuthenticator = - document.getElementById('platform_authenticator').value === 'true'; + const platformAuthenticator = document.getElementById('platform_authenticator').value === 'true'; - enrollWebauthnDevice({ - userId: document.getElementById('user_id').value, - userEmail: document.getElementById('user_email').value, - userChallenge: document.getElementById('user_challenge').value, - excludeCredentials: document.getElementById('exclude_credentials').value, - platformAuthenticator, + enrollWebauthnDevice({ + userId: document.getElementById('user_id').value, + userEmail: document.getElementById('user_email').value, + userChallenge: document.getElementById('user_challenge').value, + excludeCredentials: document.getElementById('exclude_credentials').value, + platformAuthenticator, + }) + .then((result) => { + document.getElementById('webauthn_id').value = result.webauthnId; + document.getElementById('webauthn_public_key').value = result.webauthnPublicKey; + document.getElementById('attestation_object').value = result.attestationObject; + document.getElementById('client_data_json').value = result.clientDataJSON; + document.getElementById('webauthn_form').submit(); }) - .then((result) => { - document.getElementById('webauthn_id').value = result.webauthnId; - document.getElementById('webauthn_public_key').value = result.webauthnPublicKey; - document.getElementById('attestation_object').value = result.attestationObject; - document.getElementById('client_data_json').value = result.clientDataJSON; - document.getElementById('webauthn_form').submit(); - }) - .catch((err) => reloadWithError(err.name, { force: true })); - }); - const input = document.getElementById('nickname'); - input.addEventListener('keypress', function (event) { - if (event.keyCode === 13) { - // prevent form submit - event.preventDefault(); - } - }); - input.addEventListener('keyup', function (event) { + .catch((err) => reloadWithError(err.name, { force: true })); +}); +const input = document.getElementById('nickname'); +input.addEventListener('keypress', function (event) { + if (event.keyCode === 13) { + // prevent form submit event.preventDefault(); - if (event.keyCode === 13 && input.value) { - continueButton.click(); - } - }); -} - -loadPolyfills(['url']).then(webauthn); + } +}); +input.addEventListener('keyup', function (event) { + event.preventDefault(); + if (event.keyCode === 13 && input.value) { + continueButton.click(); + } +}); diff --git a/app/javascript/packs/webauthn-unhide.js b/app/javascript/packs/webauthn-unhide.js index 480b1b40dac..e71f3ea5057 100644 --- a/app/javascript/packs/webauthn-unhide.js +++ b/app/javascript/packs/webauthn-unhide.js @@ -1,7 +1,6 @@ -import { loadPolyfills } from '@18f/identity-polyfill'; import { isWebAuthnEnabled } from '../app/webauthn'; -export async function unhideWebauthn() { +(async () => { Object.entries({ select_webauthn: isWebAuthnEnabled(), select_webauthn_platform: await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable(), @@ -20,8 +19,4 @@ export async function unhideWebauthn() { checkboxes[i + 1].checked = true; } -} - -if (process.env.NODE_ENV !== 'test') { - loadPolyfills(['classlist']).then(unhideWebauthn); -} +})(); diff --git a/babel.config.js b/babel.config.js index ba67e7d3d9e..b00d49da1cd 100644 --- a/babel.config.js +++ b/babel.config.js @@ -39,7 +39,7 @@ module.exports = function (api) { useBuiltIns: 'usage', corejs: 3, modules: false, - exclude: ['transform-typeof-symbol'], + exclude: ['transform-typeof-symbol', 'web.url', 'es.promise'], }, ], ].filter(Boolean), diff --git a/webpack.config.js b/webpack.config.js index 5319e7e58aa..8567d899f13 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -61,7 +61,7 @@ module.exports = /** @type {import('webpack').Configuration} */ ({ }, optimization: { chunkIds: 'natural', - splitChunks: { chunks: 'all' }, + splitChunks: { chunks: (chunk) => chunk.name !== 'polyfill' }, }, plugins: [ new WebpackAssetsManifest({ diff --git a/yarn.lock b/yarn.lock index 7d883a77846..5b604aa83d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5207,6 +5207,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +promise-polyfill@^8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6" + integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg== + prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" From 1c437dbf40fbadb0c1ce05f4c4dcc862141eefde Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 14 Mar 2022 15:24:38 -0400 Subject: [PATCH 2/7] Restore test env auto-initialize exclusion --- app/javascript/packs/webauthn-unhide.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/packs/webauthn-unhide.js b/app/javascript/packs/webauthn-unhide.js index e71f3ea5057..7f81040b4a4 100644 --- a/app/javascript/packs/webauthn-unhide.js +++ b/app/javascript/packs/webauthn-unhide.js @@ -1,6 +1,6 @@ import { isWebAuthnEnabled } from '../app/webauthn'; -(async () => { +export async function unhideWebauthn() { Object.entries({ select_webauthn: isWebAuthnEnabled(), select_webauthn_platform: await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable(), @@ -19,4 +19,8 @@ import { isWebAuthnEnabled } from '../app/webauthn'; checkboxes[i + 1].checked = true; } -})(); +} + +if (process.env.NODE_ENV !== 'test') { + unhideWebauthn(); +} From 45f9eeb62a98359ff9c242e7e23f4b9a9dc836da Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 14 Mar 2022 15:30:10 -0400 Subject: [PATCH 3/7] Fix webauthn-setup specs Restore initializer function, exclude from test side-effect via similar pattern as webauthn-unhide script --- app/javascript/packs/webauthn-setup.js | 77 ++++++++++--------- spec/javascripts/packs/webauthn-setup-spec.js | 8 +- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/app/javascript/packs/webauthn-setup.js b/app/javascript/packs/webauthn-setup.js index 5ec9037287f..fbb28d9f906 100644 --- a/app/javascript/packs/webauthn-setup.js +++ b/app/javascript/packs/webauthn-setup.js @@ -15,42 +15,49 @@ export function reloadWithError(error, { force = false } = {}) { } } -if (!isWebAuthnEnabled()) { - reloadWithError('NotSupportedError'); -} -const continueButton = document.getElementById('continue-button'); -continueButton.addEventListener('click', () => { - document.getElementById('spinner').classList.remove('hidden'); - document.getElementById('continue-button').className = 'hidden'; +function webauthn() { + if (!isWebAuthnEnabled()) { + reloadWithError('NotSupportedError'); + } + const continueButton = document.getElementById('continue-button'); + continueButton.addEventListener('click', () => { + document.getElementById('spinner').classList.remove('hidden'); + document.getElementById('continue-button').className = 'hidden'; - const platformAuthenticator = document.getElementById('platform_authenticator').value === 'true'; + const platformAuthenticator = + document.getElementById('platform_authenticator').value === 'true'; - enrollWebauthnDevice({ - userId: document.getElementById('user_id').value, - userEmail: document.getElementById('user_email').value, - userChallenge: document.getElementById('user_challenge').value, - excludeCredentials: document.getElementById('exclude_credentials').value, - platformAuthenticator, - }) - .then((result) => { - document.getElementById('webauthn_id').value = result.webauthnId; - document.getElementById('webauthn_public_key').value = result.webauthnPublicKey; - document.getElementById('attestation_object').value = result.attestationObject; - document.getElementById('client_data_json').value = result.clientDataJSON; - document.getElementById('webauthn_form').submit(); + enrollWebauthnDevice({ + userId: document.getElementById('user_id').value, + userEmail: document.getElementById('user_email').value, + userChallenge: document.getElementById('user_challenge').value, + excludeCredentials: document.getElementById('exclude_credentials').value, + platformAuthenticator, }) - .catch((err) => reloadWithError(err.name, { force: true })); -}); -const input = document.getElementById('nickname'); -input.addEventListener('keypress', function (event) { - if (event.keyCode === 13) { - // prevent form submit + .then((result) => { + document.getElementById('webauthn_id').value = result.webauthnId; + document.getElementById('webauthn_public_key').value = result.webauthnPublicKey; + document.getElementById('attestation_object').value = result.attestationObject; + document.getElementById('client_data_json').value = result.clientDataJSON; + document.getElementById('webauthn_form').submit(); + }) + .catch((err) => reloadWithError(err.name, { force: true })); + }); + const input = document.getElementById('nickname'); + input.addEventListener('keypress', function (event) { + if (event.keyCode === 13) { + // prevent form submit + event.preventDefault(); + } + }); + input.addEventListener('keyup', function (event) { event.preventDefault(); - } -}); -input.addEventListener('keyup', function (event) { - event.preventDefault(); - if (event.keyCode === 13 && input.value) { - continueButton.click(); - } -}); + if (event.keyCode === 13 && input.value) { + continueButton.click(); + } + }); +} + +if (process.env.NODE_ENV !== 'test') { + webauthn(); +} diff --git a/spec/javascripts/packs/webauthn-setup-spec.js b/spec/javascripts/packs/webauthn-setup-spec.js index 60349671519..df52adb6f2d 100644 --- a/spec/javascripts/packs/webauthn-setup-spec.js +++ b/spec/javascripts/packs/webauthn-setup-spec.js @@ -1,17 +1,13 @@ import { useSandbox } from '../support/sinon'; import useDefineProperty from '../support/define-property'; +import { reloadWithError } from '../../../app/javascript/packs/webauthn-setup'; describe('webauthn-setup', () => { const defineProperty = useDefineProperty(); const sandbox = useSandbox(); - let reloadWithError; - beforeEach(async () => { + beforeEach(() => { defineProperty(window, 'location', { value: { search: null } }); - - // Because webauthn-setup has side effects which trigger a call to `window.location.search`, - // ensure that basic stubbing is in place before importing from the file. - ({ reloadWithError } = await import('../../../app/javascript/packs/webauthn-setup')); }); describe('reloadWithError', () => { From c375c1060a43b40dd9f4dee51c285a5247626b1a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 14 Mar 2022 16:37:25 -0400 Subject: [PATCH 4/7] Fix ScriptHelper spec --- app/helpers/script_helper.rb | 4 ++-- spec/helpers/script_helper_spec.rb | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb index 7eb47ef47b9..7e01e68afd2 100644 --- a/app/helpers/script_helper.rb +++ b/app/helpers/script_helper.rb @@ -22,11 +22,11 @@ def javascript_packs_tag_once(*names, prepend: false) def render_javascript_pack_once_tags(*names) javascript_packs_tag_once(*names) if names.present? - if @scripts.present? + if @scripts && (sources = AssetSources.get_sources(*@scripts)).present? safe_join( [ javascript_include_tag(*AssetSources.get_sources('polyfill'), nomodule: ''), - javascript_include_tag(*AssetSources.get_sources(*@scripts)), + javascript_include_tag(*sources), ], ) end diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb index eae69b57562..7c76a36e02e 100644 --- a/spec/helpers/script_helper_spec.rb +++ b/spec/helpers/script_helper_spec.rb @@ -30,6 +30,7 @@ before do javascript_packs_tag_once('document-capture', 'document-capture') javascript_packs_tag_once('application', prepend: true) + allow(AssetSources).to receive(:get_sources).with('polyfill').and_return(['/polyfill.js']) allow(AssetSources).to receive(:get_sources).with('application', 'document-capture'). and_return(['/application.js', '/document-capture.js']) end @@ -38,7 +39,9 @@ output = render_javascript_pack_once_tags expect(output).to have_css( - "script[src^='/application.js'] ~ script[src^='/document-capture.js']", + "script[src^='/polyfill.js'][nomodule] ~ \ + script[src^='/application.js'] ~ \ + script[src^='/document-capture.js']", count: 1, visible: :all, ) @@ -47,6 +50,7 @@ context 'with named scripts argument' do before do + allow(AssetSources).to receive(:get_sources).with('polyfill').and_return(['/polyfill.js']) allow(AssetSources).to receive(:get_sources).with('application'). and_return(['/application.js']) end @@ -54,7 +58,10 @@ it 'enqueues those scripts before printing them' do output = render_javascript_pack_once_tags('application') - expect(output).to have_css('script[src="/application.js"]', visible: :all) + expect(output).to have_css( + "script[src^='/polyfill.js'][nomodule] ~ script[src='/application.js']", + visible: :all, + ) end end @@ -64,7 +71,7 @@ end it 'gracefully outputs nothing' do - expect(render_javascript_pack_once_tags).to be_empty + expect(render_javascript_pack_once_tags).to be_nil end end end From f02d7d2a69e38b343f89d467293e1085c2995186 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 14 Mar 2022 16:49:51 -0400 Subject: [PATCH 5/7] Exclude URLSearchParams polyfill, document --- babel.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/babel.config.js b/babel.config.js index b00d49da1cd..08165281bce 100644 --- a/babel.config.js +++ b/babel.config.js @@ -39,7 +39,9 @@ module.exports = function (api) { useBuiltIns: 'usage', corejs: 3, modules: false, - exclude: ['transform-typeof-symbol', 'web.url', 'es.promise'], + // Exclude polyfills for features known to be provided by @18f/identity-polyfill package. + // See: https://github.com/babel/babel-polyfills/blob/main/packages/babel-plugin-polyfill-corejs3/src/built-in-definitions.js + exclude: ['web.url', 'web.url-search-params', 'es.promise'], }, ], ].filter(Boolean), From 2aab4c08370255c2f2a24d76001037700760fc43 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 14 Mar 2022 16:53:30 -0400 Subject: [PATCH 6/7] changelog: Improvements, Optimization, Skip polyfill load behavior for modern browsers From d9dbfa9c430efb78fb62ae91aa79a19f26f1bac5 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 14 Mar 2022 19:07:28 -0400 Subject: [PATCH 7/7] Prevent preload header for polyfill pack **Why**: Since it won't be loaded by modern browsers, and since those modern browsers like to complain about preload header not followed by actual load of the script --- app/helpers/script_helper.rb | 29 +++++++++++++++++------------ spec/helpers/script_helper_spec.rb | 3 ++- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb index 7e01e68afd2..c6c0e49e053 100644 --- a/app/helpers/script_helper.rb +++ b/app/helpers/script_helper.rb @@ -1,11 +1,7 @@ # rubocop:disable Rails/HelperInstanceVariable module ScriptHelper - def javascript_include_tag_without_preload(*sources) - original_preload_links_header = ActionView::Helpers::AssetTagHelper.preload_links_header - ActionView::Helpers::AssetTagHelper.preload_links_header = false - tag = javascript_include_tag(*sources) - ActionView::Helpers::AssetTagHelper.preload_links_header = original_preload_links_header - tag + def javascript_include_tag_without_preload(...) + without_preload_links_header { javascript_include_tag(...) } end def javascript_packs_tag_once(*names, prepend: false) @@ -23,13 +19,22 @@ def javascript_packs_tag_once(*names, prepend: false) def render_javascript_pack_once_tags(*names) javascript_packs_tag_once(*names) if names.present? if @scripts && (sources = AssetSources.get_sources(*@scripts)).present? - safe_join( - [ - javascript_include_tag(*AssetSources.get_sources('polyfill'), nomodule: ''), - javascript_include_tag(*sources), - ], - ) + safe_join([javascript_polyfill_pack_tag, javascript_include_tag(*sources)]) end end + + private + + def javascript_polyfill_pack_tag + javascript_include_tag_without_preload(*AssetSources.get_sources('polyfill'), nomodule: '') + end + + def without_preload_links_header + original_preload_links_header = ActionView::Helpers::AssetTagHelper.preload_links_header + ActionView::Helpers::AssetTagHelper.preload_links_header = false + result = yield + ActionView::Helpers::AssetTagHelper.preload_links_header = original_preload_links_header + result + end end # rubocop:enable Rails/HelperInstanceVariable diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb index 7c76a36e02e..f8cee5fcce4 100644 --- a/spec/helpers/script_helper_spec.rb +++ b/spec/helpers/script_helper_spec.rb @@ -5,9 +5,10 @@ describe '#javascript_include_tag_without_preload' do it 'avoids modifying headers' do - javascript_include_tag_without_preload 'application' + output = javascript_include_tag_without_preload 'application' expect(response.header['Link']).to be_nil + expect(output).to have_css('script', visible: :all) end end