-
Notifications
You must be signed in to change notification settings - Fork 333
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
Throw SupportError
when instantiating components where GOV.UK Frontend is not supported
#4030
Conversation
d6e292d
to
85c50f6
Compare
85c50f6
to
41813df
Compare
41813df
to
a533c7e
Compare
a533c7e
to
8ecc243
Compare
8ecc243
to
9fd3f16
Compare
9fd3f16
to
216600f
Compare
216600f
to
439da96
Compare
@@ -103,6 +104,7 @@ export { | |||
Checkboxes, | |||
ErrorSummary, | |||
ExitThisPage, | |||
GOVUKFrontendError, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only export our base GOVUKFrontendError
rather than all error classes with the aim of:
- allowing separating our own errors from native ones in bulk, which users can do running
instanceof GOVUKFrontendError
- but not crowding our export with each error we throw, as users can check the type of error through their
name
property
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fab work in this PR Romaric
Do we want to discuss if we're ready for errors to be part of our "public" API?
Bit like the I18n class where we chose not to export it
(Gives us freedom to keep iterating without breaking changes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Up for discussion, I'll revive the thread on Slack where I proposed the approach I implemented here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's default to not exporting it yet, can always add it in another PR
* @abstract | ||
*/ | ||
export class GOVUKFrontendError extends Error { | ||
name = 'GOVUKFrontendError' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annoying, but necessary. Simply extending Error
doesn't give the error a new name, unfortunately.
And going for something automated like this.name = this.constructor.name
in GOVUKFrontendError
's constructor
means the error gets a mangled name once minifier have run over the code and rename the class to say n
. While we may prevent the mangling in our files, we don't control how services minify theirs, so hardcoding the name limits risks of having an error with the name of n
.
@@ -133,6 +133,40 @@ Avoid using namespace imports (`import * as namespace`) in code bundled for Comm | |||
|
|||
Prefer named exports over default exports to avoid compatibility issues with transpiler "synthetic default" as discussed in: https://github.com/alphagov/govuk-frontend/issues/2829 | |||
|
|||
## Throwing errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a starter for 10 listing the spirit in which the errors have been implemented in this PR. Happy to discuss and update.
@@ -113,7 +114,11 @@ export class Accordion { | |||
* @param {AccordionConfig} [config] - Accordion config | |||
*/ | |||
constructor ($module, config) { | |||
if (!($module instanceof HTMLElement) || !document.body.classList.contains('govuk-frontend-supported')) { | |||
if (!document.body.classList.contains('govuk-frontend-supported')) { | |||
throw new GOVUKFrontendNotSupportedError() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a random thought
Similar to how TypeError
isn't NotCorrectTypeError
, should we flip out the Not
?
throw new GOVUKFrontendNotSupportedError() | |
throw new GOVUKFrontendSupportError() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair, and that makes it shorter, I'll rename 😊
renderAndInitialise(page, 'accordion', { | ||
params: examples.default, | ||
beforeInitialisation () { | ||
document.body.classList.remove('govuk-frontend-supported') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
@@ -648,7 +670,7 @@ describe('Character count', () => { | |||
// Override maxlength to 10 | |||
maxlength: 10 | |||
}, | |||
initialiser ($module) { | |||
beforeInitialisation ($module) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it's no longer with us, as a sign of respect for .init()
shall we rename this? 😆
Suppose it matches initAll()
a bit better too
beforeInitialisation ($module) { | |
beforeInit ($module) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not calling initAll
as a way to intialise and init
is gone, so I'm not sure we should go for the abreviated version. Could shorten it to before
but I'd rather be verbose in case we add other before
moments (though that'd probably be a hint for refactoring).
// for example if they initialise the component | ||
// on their own by directly passing the result | ||
// of `document.querySelector`. | ||
// To avoid breaking further JavaScript initialisation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was quite fond of this message, all on its own in ErrorSummary
Thoughts on adding a little disclaimer to every component?
- Error throws
- Error console shows stack trace
- Stack trace uses original source via source maps
- Developer clicks to these lines and reads helpful message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think most of that info should be summarised in the error message when implementing https://github.com/alphagov/govuk-frontend/issues/4035
.
Regarding adding things to each components, I think this would rather be the role of a base Component class. I think with the error throwing for GOV.UK Frontend support and the type check of $module
there's enough ground for creating it already (only to add new features to it, not refactor existing code into it) and centralise these checks and possible comments.
@@ -37,6 +37,7 @@ describe('GOV.UK Frontend', () => { | |||
'Checkboxes', | |||
'ErrorSummary', | |||
'ExitThisPage', | |||
'GOVUKFrontendError', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to https://github.com/alphagov/govuk-frontend/pull/4030/files#r1284215663
Should we not export our errors in the default export, we'll need to manage Terser mangling:
govuk-frontend/packages/govuk-frontend/rollup.release.config.mjs
Lines 31 to 33 in 91d0a5d
terser({ | |
format: { comments: false }, | |
mangle: { reserved: Object.keys(GOVUKFrontend) }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you expand on what we'll need to manage on the Terser side if we don't export the errors internally?
If it's about the error naming after mangling, the hardcoded name
property is there to handle the situation (especially as we can't guarantee that library users will take the same step as us towards not mangling the name of the errors/our exports).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that mangle.reserved
option
But if the mangled error class name doesn't appear in stack traces then all good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment about mangling on this PR: #4075.
The name is clear in the logged message, but mangled in the stacktrace. I think mangling is worth its own discussion, as it may be worth disabling it altogether rather than keep maintaining an evergrowing list of names to not mangle for our min.js
file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And opened #4076 to explore our options.
it('has the correct tabindex attribute to be focused with JavaScript', async () => { | ||
await goToComponent(page, 'notification-banner', { | ||
exampleName: 'with-type-as-success' | ||
describe('when type is set to "success"', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice little test refactor with the describe()
nesting, mind if we split it into another commit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No probs, I'll split the extra describe
s in their own commit to separate them from the implementation of the error throwing.
await page.setJavaScriptEnabled(false) | ||
}) | ||
describe('Radios', () => { | ||
describe('with conditional reveals', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with this one, separate commit?
Jest output is improved too ⭐
@@ -0,0 +1,33 @@ | |||
/** | |||
* A base class for `Error`s thrown by GOV.UK Frontend. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mind if we keep a short and sweet "title" at the top for any generated docs?
* A base class for `Error`s thrown by GOV.UK Frontend. | |
* GOV.UK Frontend error | |
* | |
* A base class for errors thrown by GOV.UK Frontend. |
* to be thrown by our code. | ||
* | ||
* @example | ||
* ```js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder to use mjs
over js
for ES modules code blocks
Can't remember if ESLint is set up for these (like it is for Markdown) but it might matter one day
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example doesn't use anything module specific (import
/export
) so plain js
is fine there 😊
Regarding ESLint, it doesn't seem we've enabled jsdoc/check-examples. We can discuss its enabling in a separate part as it may have some tricky side effects (eg. requiring this class to have its own JSDoc block inside the example).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah not so much the module specific stuff
We tend to use *.mjs
as the default now, so some rules like the TypeScript parser are only for *.mjs
govuk-frontend/packages/govuk-frontend/.eslintrc.js
Lines 11 to 13 in e1ef6d2
files: ['src/govuk/**/*.mjs'], | |
excludedFiles: ['**/*.test.mjs'], | |
parser: '@typescript-eslint/parser', |
* name = "MissingRootError" | ||
* } | ||
* ``` | ||
* @abstract |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah do we not want to use this class directly, never ever?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that's the intent. If we don't create subclasses with their own names, that means updating an error's message becomes a breaking change, as the message would be the only way to distinguish one error from another. Subclassing (with a specific name
) ensures we can adjust the wording of the messages freely as the name
(or class itself should we/when we export it) is the public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth enforcing this in the constructor too?
export class GOVUKFrontendError extends Error {
name = 'GOVUKFrontendError'
constructor () {
if (this.constructor == GOVUKFrontendError) {
throw new Error("GOVUKFrontendError is an abstract class and can't be instantiated");
}
}
}
Or is errors throwing errors too confusing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure it's worth enforcing the @abstract
in code. I'd rather lean towards shipping less code and if we get evidence that people misuse it, clamp down on it rather than be pre-emptively defensive 😊
export class GOVUKFrontendNotSupportedError extends GOVUKFrontendError { | ||
name = 'GOVUKFrontendNotSupportedError' | ||
|
||
/** */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/** */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, good spot! 🦅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, no, that wasn't whoops, it's making ESLint happy as there's not much to document on that constructor 🤔 I'll replace with a more explicit disabling of ESLint for that line 😊
8530b5f
to
dc09a42
Compare
dc09a42
to
d683de5
Compare
d683de5
to
702d383
Compare
702d383
to
7aa2be2
Compare
7aa2be2
to
37713cf
Compare
* @param {HTMLElement} [$scope] - The `<body>` element of the document to check for support | ||
* @returns {boolean} Whether GOV.UK Frontend is supported on this page | ||
*/ | ||
export function isSupported($scope = document.body) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have browser support for parameter defaults without Babel polyfills?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, Babel leaves that line as is in dist
. Support looks a bit older than what we're configured to support. And if transpilation happened, there's no polyfills involved. So all good there 🎉
…ialise` - Clarify naming as it's actually code that runs before initialisation rather than does any kind of initialisation of the component itself - Update how the hook is run to be directly called by `page.evaluate`. Having it run as part of the function that did the initialisation didn't run reliably for updating the `govuk-frontend-supported` class, or even other simpler scenarios like logging or throwing. Calling it directly with `page.evaluate` seems to run more reliably.
Adds a couple of extra `describe` to group existing tests in anticipation of new tests sections for the errors
Allows components to indicate they didn't inistantiate because GOV.UK Frontend is not supported
It's the same code for all components and the architecture we're looking to implement so we may as well start introducing it, keeping it internal
37713cf
to
98eaeca
Compare
SupportError
when instantiating components where GOV.UK Frontend is not supported
Remove stray `.only()` from PR #4030
Remove stray `.only()` from PR #4030
This PR updates all our JavaScript components to throw a
GOVUKFrontendNotSupportedError
if they get instantiated where GOV.UK Frontend is not supported.Besides updating the components to throw this new error the PR:
GOVUKFrontendError
for our errors to subclass. It allows users to easily separate our custom errors from regular JavaScript errors usinginstanceof
.renderAndInitialise
to make running code before the component's initialisation more reliable (had issues where theinitialiser
– now renamedbeforeInitialisation
for clarity – didn't execute when run in the middle of the block instantiating the component).Closes #4009