forked from redwoodjs/redwood
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(crwa): Explicit check for possible null value in `entry.client.ts…
…x` (redwoodjs#9251) **Problem** Fixes redwoodjs#9059 which is a type error raised when you have strict type checking enabled. The `getElementById` call can return null as the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById#return_value) mention. **Changes** Adds a basic null check taken from the solution given in redwoodjs#9059. **Considerations** This adds some boilerplate code to a user facing file but I think it's a reasonable addition. It's readable and clear what the code is doing without the need for further explanation or technical knowledge. In the case where this causes a runtime error then the message is clearer: ![Screenshot from 2023-10-02 23-16-38](https://github.com/redwoodjs/redwood/assets/56300765/ca6d51bd-2c28-438e-87d3-7411e54721ec) I haven't immediately been able to think of some hidden solution to this that isn't exposed to the user in some boilerplate code.
- Loading branch information
1 parent
229bd4b
commit b0964a9
Showing
15 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
packages/codemods/src/codemods/v6.x.x/entryClientNullCheck/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# Entry Client Null Check | ||
|
||
|
||
**Description** | ||
|
||
When you enable typescript strict mode the return type of the `getElementById` function will be `HTMLElement | null`. This means that you need to check if the element exists before using it. This codemod adds a check to ensure the element exists before using it. | ||
|
||
You will also see a clearer error message in the browser console if the element does not exist. | ||
|
||
**Examples** | ||
|
||
For example the following `entry.client.tsx`: | ||
```tsx | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} | ||
|
||
``` | ||
would become: | ||
```tsx | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
if (!redwoodAppElement) { | ||
throw new Error( | ||
"Could not find an element with ID 'redwood-app'. Please ensure it exists in your 'web/src/index.html' file." | ||
) | ||
} | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} | ||
|
||
``` | ||
where a check to ensure the element exists has been added. | ||
|
24 changes: 24 additions & 0 deletions
24
...emods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/alreadyChecking.input.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
// Some user is already checking for null | ||
if(redwoodAppElement === null){ | ||
throw new Error( | ||
"Opps I must have changed the div name or deleted the div completely!" | ||
) | ||
} | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} |
30 changes: 30 additions & 0 deletions
30
...mods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/alreadyChecking.output.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
if (!redwoodAppElement) { | ||
throw new Error( | ||
"Could not find an element with ID 'redwood-app'. Please ensure it exists in your 'web/src/index.html' file." | ||
) | ||
} | ||
|
||
// Some user is already checking for null | ||
if(redwoodAppElement === null){ | ||
throw new Error( | ||
"Opps I must have changed the div name or deleted the div completely!" | ||
) | ||
} | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} |
17 changes: 17 additions & 0 deletions
17
...ages/codemods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/default.input.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} |
23 changes: 23 additions & 0 deletions
23
...ges/codemods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/default.output.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
if (!redwoodAppElement) { | ||
throw new Error( | ||
"Could not find an element with ID 'redwood-app'. Please ensure it exists in your 'web/src/index.html' file." | ||
) | ||
} | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} |
24 changes: 24 additions & 0 deletions
24
...ges/codemods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/moreCode.input.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
// Some random additional user code | ||
if(Math.random() > 0.5){ | ||
console.log("Some random code execution...") | ||
}else{ | ||
console.log("There are", redwoodAppElement.children?.length, "div children in the redwood-app element.") | ||
} | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} |
30 changes: 30 additions & 0 deletions
30
...es/codemods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/moreCode.output.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const redwoodAppElement = document.getElementById('redwood-app') | ||
|
||
if (!redwoodAppElement) { | ||
throw new Error( | ||
"Could not find an element with ID 'redwood-app'. Please ensure it exists in your 'web/src/index.html' file." | ||
) | ||
} | ||
|
||
// Some random additional user code | ||
if(Math.random() > 0.5){ | ||
console.log("Some random code execution...") | ||
}else{ | ||
console.log("There are", redwoodAppElement.children?.length, "div children in the redwood-app element.") | ||
} | ||
|
||
if (redwoodAppElement.children?.length > 0) { | ||
hydrateRoot(redwoodAppElement, <App />) | ||
} else { | ||
const root = createRoot(redwoodAppElement) | ||
root.render(<App />) | ||
} |
17 changes: 17 additions & 0 deletions
17
...demods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/unintelligible.input.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const hahaRenamedVariable = document.getElementById('redwood-app-custom-div-id') | ||
|
||
if (hahaRenamedVariable.children?.length > 0 || Math.random() > 0.5) { | ||
hydrateRoot(hahaRenamedVariable, <App />) | ||
} else { | ||
const root = createRoot(hahaRenamedVariable) | ||
root.render(<App />) | ||
} |
17 changes: 17 additions & 0 deletions
17
...emods/src/codemods/v6.x.x/entryClientNullCheck/__testfixtures__/unintelligible.output.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { hydrateRoot, createRoot } from 'react-dom/client' | ||
|
||
import App from './App' | ||
/** | ||
* When `#redwood-app` isn't empty then it's very likely that you're using | ||
* prerendering. So React attaches event listeners to the existing markup | ||
* rather than replacing it. | ||
* https://reactjs.org/docs/react-dom-client.html#hydrateroot | ||
*/ | ||
const hahaRenamedVariable = document.getElementById('redwood-app-custom-div-id') | ||
|
||
if (hahaRenamedVariable.children?.length > 0 || Math.random() > 0.5) { | ||
hydrateRoot(hahaRenamedVariable, <App />) | ||
} else { | ||
const root = createRoot(hahaRenamedVariable) | ||
root.render(<App />) | ||
} |
17 changes: 17 additions & 0 deletions
17
.../codemods/src/codemods/v6.x.x/entryClientNullCheck/__tests__/entryClientNullCheck.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
describe('entryClientNullCheck', () => { | ||
it('Handles the default case', async () => { | ||
await matchTransformSnapshot('entryClientNullCheck', 'default') | ||
}) | ||
|
||
it('User has already implemented the check', async () => { | ||
await matchTransformSnapshot('entryClientNullCheck', 'alreadyChecking') | ||
}) | ||
|
||
it('Additional code present', async () => { | ||
await matchTransformSnapshot('entryClientNullCheck', 'moreCode') | ||
}) | ||
|
||
it('Unintelligible changes to entry file', async () => { | ||
await matchTransformSnapshot('entryClientNullCheck', 'unintelligible') | ||
}) | ||
}) |
37 changes: 37 additions & 0 deletions
37
packages/codemods/src/codemods/v6.x.x/entryClientNullCheck/entryClientNullCheck.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import type { FileInfo, API } from 'jscodeshift' | ||
|
||
export default function transform(file: FileInfo, api: API) { | ||
const j = api.jscodeshift | ||
const ast = j(file.source) | ||
|
||
// Get the expected variable declaration | ||
const node = ast.find(j.VariableDeclaration, { | ||
declarations: [{ id: { name: 'redwoodAppElement' } }], | ||
}) | ||
|
||
// If it doesn't exist, bail out and let the user know | ||
if (node.length === 0) { | ||
console.warn( | ||
"\nCould not find 'redwoodAppElement' variable declaration. Please make the necessary changes to your 'web/src/index.js' file manually.\n" | ||
) | ||
return file.source | ||
} | ||
|
||
// Insert the new null check | ||
node.insertAfter( | ||
j.ifStatement( | ||
j.unaryExpression('!', j.identifier('redwoodAppElement')), | ||
j.blockStatement([ | ||
j.throwStatement( | ||
j.newExpression(j.identifier('Error'), [ | ||
j.literal( | ||
"Could not find an element with ID 'redwood-app'. Please ensure it exists in your 'web/src/index.html' file." | ||
), | ||
]) | ||
), | ||
]) | ||
) | ||
) | ||
|
||
return ast.toSource() | ||
} |
26 changes: 26 additions & 0 deletions
26
packages/codemods/src/codemods/v6.x.x/entryClientNullCheck/entryClientNullCheck.yargs.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import path from 'path' | ||
|
||
import fg from 'fast-glob' | ||
import type { TaskInnerAPI } from 'tasuku' | ||
import task from 'tasuku' | ||
|
||
import { getPaths } from '@redwoodjs/project-config' | ||
|
||
import runTransform from '../../../lib/runTransform' | ||
|
||
export const command = 'entry-client-null-check' | ||
export const description = '(v6.x.x->v6.x.x) Converts world to bazinga' | ||
|
||
export const handler = () => { | ||
task('Entry Client Null Check', async ({ setOutput }: TaskInnerAPI) => { | ||
await runTransform({ | ||
transformPath: path.join(__dirname, 'entryClientNullCheck.js'), | ||
targetPaths: fg.sync('entry.client.{jsx,tsx}', { | ||
cwd: getPaths().web.src, | ||
absolute: true, | ||
}), | ||
}) | ||
|
||
setOutput('All done! Run `yarn rw lint --fix` to prettify your code') | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters