Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract dedicated polyfill package #6060

Merged
merged 7 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions app/components/clipboard_button_component.js
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 2 additions & 4 deletions app/components/phone_input_component.js
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 2 additions & 4 deletions app/components/time_component.js
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 2 additions & 4 deletions app/components/validated_field_component.js
Original file line number Diff line number Diff line change
@@ -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);
26 changes: 19 additions & 7 deletions app/helpers/script_helper.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -22,7 +18,23 @@ 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 && (sources = AssetSources.get_sources(*@scripts)).present?
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
7 changes: 2 additions & 5 deletions app/javascript/packages/clipboard-button/index.spec.js
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
73 changes: 8 additions & 65 deletions app/javascript/packages/polyfill/index.js
Original file line number Diff line number Diff line change
@@ -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<SupportedPolyfills,Polyfill>}
*/
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';
17 changes: 0 additions & 17 deletions app/javascript/packages/polyfill/is-safe.js

This file was deleted.

17 changes: 0 additions & 17 deletions app/javascript/packages/polyfill/is-safe.spec.js

This file was deleted.

1 change: 1 addition & 0 deletions app/javascript/packages/polyfill/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promises were only polyfilled previously because we use them in our own code, via @babel/preset-env's useBuiltIns: 'usage' option. By default, third-party packages are excluded from Babel transforms. Since some of the polyfills themselves rely on the Promise polyfill (fetch, webcrypto-shim), it needs to be explicitly included.

"webcrypto-shim": "^0.1.7",
"whatwg-fetch": "^3.4.0"
}
Expand Down
21 changes: 9 additions & 12 deletions app/javascript/packs/doc-capture-polling.js
Original file line number Diff line number Diff line change
@@ -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();
5 changes: 2 additions & 3 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -125,7 +124,7 @@ function addPageAction(action) {
const noticeError = (error) =>
/** @type {DocumentCaptureGlobal} */ (window).newrelic?.noticeError(error);

loadPolyfills(['fetch', 'crypto', 'url']).then(async () => {
(async () => {
aduth marked this conversation as resolved.
Show resolved Hide resolved
const backgroundUploadURLs = getBackgroundUploadURLs();
const isAsyncForm = Object.keys(backgroundUploadURLs).length > 0;
const csrf = getMetaContent('csrf-token');
Expand Down Expand Up @@ -211,4 +210,4 @@ loadPolyfills(['fetch', 'crypto', 'url']).then(async () => {
);

render(<App />, appRoot);
});
})();
7 changes: 2 additions & 5 deletions app/javascript/packs/form-steps-wait.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render, unmountComponentAtNode } from 'react-dom';
import { Alert } from '@18f/identity-components';
import { loadPolyfills } from '@18f/identity-polyfill';

/**
* @typedef FormStepsWaitElements
Expand Down Expand Up @@ -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());
10 changes: 3 additions & 7 deletions app/javascript/packs/form-validation.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
3 changes: 1 addition & 2 deletions app/javascript/packs/one-time-code-input.js
Original file line number Diff line number Diff line change
@@ -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();
}
1 change: 1 addition & 0 deletions app/javascript/packs/polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@18f/identity-polyfill';
5 changes: 3 additions & 2 deletions app/javascript/packs/webauthn-setup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { loadPolyfills } from '@18f/identity-polyfill';
import { isWebAuthnEnabled, enrollWebauthnDevice } from '../app/webauthn';

/**
Expand Down Expand Up @@ -59,4 +58,6 @@ function webauthn() {
});
}

loadPolyfills(['url']).then(webauthn);
if (process.env.NODE_ENV !== 'test') {
webauthn();
}
3 changes: 1 addition & 2 deletions app/javascript/packs/webauthn-unhide.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { loadPolyfills } from '@18f/identity-polyfill';
import { isWebAuthnEnabled } from '../app/webauthn';

export async function unhideWebauthn() {
Expand All @@ -23,5 +22,5 @@ export async function unhideWebauthn() {
}

if (process.env.NODE_ENV !== 'test') {
loadPolyfills(['classlist']).then(unhideWebauthn);
unhideWebauthn();
}
4 changes: 3 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ module.exports = function (api) {
useBuiltIns: 'usage',
corejs: 3,
modules: false,
exclude: ['transform-typeof-symbol'],
// 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),
Expand Down
16 changes: 12 additions & 4 deletions spec/helpers/script_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,6 +31,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
Expand All @@ -38,7 +40,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,
)
Expand All @@ -47,14 +51,18 @@

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

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

Expand All @@ -64,7 +72,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
Expand Down
Loading