-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(nextjs): Wrap server-side getInitialProps #5546
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
Changes from all commits
ef89101
56aa62e
a081582
916fdb0
baa0a5a
145ed92
ad46cdb
460f113
0002873
aef9635
41ebd28
0f1390b
36f4434
cd94b12
7f0c91b
4c97ee9
3ccbe10
4c3c3ce
c0577da
ed15ca3
2f88743
adad5e3
aa5b915
b7fc5a6
62bba32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,15 +6,26 @@ | |
* manipulating them, and then turning them back into strings and appending our template code to the user's (modified) | ||
* page code. Greater detail and explanations can be found in situ in the functions below and in the helper functions in | ||
* `ast.ts`. | ||
* | ||
* For `getInitialProps` we create a virtual proxy-module that re-exports all the exports and default exports of the | ||
* original file and wraps `getInitialProps`. We do this since it allows us to very generically wrap `getInitialProps` | ||
* for all kinds ways users might define default exports (which are a lot of ways). | ||
*/ | ||
|
||
import { logger } from '@sentry/utils'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
|
||
import { isESM } from '../../utils/isESM'; | ||
import type { AST } from './ast'; | ||
import { findDeclarations, findExports, makeAST, removeComments, renameIdentifiers } from './ast'; | ||
import { | ||
findDeclarations, | ||
findExports, | ||
getExportIdentifierNames, | ||
hasDefaultExport, | ||
makeAST, | ||
removeComments, | ||
renameIdentifiers, | ||
} from './ast'; | ||
import type { LoaderThis } from './types'; | ||
|
||
// Map to keep track of each function's placeholder in the template and what it should be replaced with. (The latter | ||
|
@@ -94,44 +105,81 @@ function wrapFunctions(userCode: string, templateCode: string, filepath: string) | |
* Wrap `getStaticPaths`, `getStaticProps`, and `getServerSideProps` (if they exist) in the given page code | ||
*/ | ||
export default function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>, userCode: string): string { | ||
// We know one or the other will be defined, depending on the version of webpack being used | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const { projectDir } = this.getOptions ? this.getOptions() : this.query!; | ||
|
||
// For now this loader only works for ESM code | ||
if (!isESM(userCode)) { | ||
return userCode; | ||
} | ||
|
||
// If none of the functions we want to wrap appears in the page's code, there's nothing to do. (Note: We do this as a | ||
// simple substring match (rather than waiting until we've parsed the code) because it's meant to be an | ||
// as-fast-as-possible fail-fast. It's possible for user code to pass this check, even if it contains none of the | ||
// functions in question, just by virtue of the correct string having been found, be it in a comment, as part of a | ||
// longer variable name, etc. That said, when we actually do the code manipulation we'll be working on the code's AST, | ||
// meaning we'll be able to differentiate between code we actually want to change and any false positives which might | ||
// come up here.) | ||
if (Object.keys(DATA_FETCHING_FUNCTIONS).every(functionName => !userCode.includes(functionName))) { | ||
return userCode; | ||
} | ||
// We know one or the other will be defined, depending on the version of webpack being used | ||
const { projectDir } = 'getOptions' in this ? this.getOptions() : this.query; | ||
|
||
const templatePath = path.resolve(__dirname, '../templates/dataFetchersLoaderTemplate.js'); | ||
// make sure the template is included when runing `webpack watch` | ||
this.addDependency(templatePath); | ||
// In the following branch we will proxy the user's file. This means we return code (basically an entirely new file) | ||
// that re - exports all the user file's originial export, but with a "sentry-proxy-loader" query in the module | ||
// string. | ||
// This looks like the following: `export { a, b, c } from "[imagine userfile path here]?sentry-proxy-loader";` | ||
// Additionally, in this proxy file we import the userfile's default export, wrap `getInitialProps` on that default | ||
// export, and re -export the now modified default export as default. | ||
// Webpack will resolve the module with the "sentry-proxy-loader" query to the original file, but will give us access | ||
// to the query via`this.resourceQuery`. If we see that `this.resourceQuery` includes includes "sentry-proxy-loader" | ||
// we know we're in a proxied file and do not need to proxy again. | ||
|
||
const templateCode = fs.readFileSync(templatePath).toString(); | ||
if (!this.resourceQuery.includes('sentry-proxy-loader')) { | ||
lforst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const ast = makeAST(userCode, true); // is there a reason to ever parse without typescript? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Have you tried parsing a JS file with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "files" (strings) we're parsing in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed: Let's try a big JS file or two and see if it errs out. If it works lets simplify the makeAst logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in #5563. |
||
|
||
const [modifiedUserCode, modifiedTemplateCode] = wrapFunctions( | ||
userCode, | ||
templateCode, | ||
// Relative path to the page we're currently processing, for use in error messages | ||
path.relative(projectDir, this.resourcePath), | ||
); | ||
const exportedIdentifiers = getExportIdentifierNames(ast); | ||
|
||
// Fill in template placeholders | ||
let injectedCode = modifiedTemplateCode; | ||
for (const { placeholder, alias } of Object.values(DATA_FETCHING_FUNCTIONS)) { | ||
injectedCode = injectedCode.replace(placeholder, alias); | ||
} | ||
let outputFileContent = ''; | ||
|
||
if (exportedIdentifiers.length > 0) { | ||
outputFileContent += `export { ${exportedIdentifiers.join(', ')} } from "${ | ||
this.resourcePath | ||
}?sentry-proxy-loader";`; | ||
} | ||
|
||
if (hasDefaultExport(ast)) { | ||
lobsterkatie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
outputFileContent += ` | ||
import { default as _sentry_default } from "${this.resourcePath}?sentry-proxy-loader"; | ||
import { withSentryGetInitialProps } from "@sentry/nextjs"; | ||
|
||
return `${modifiedUserCode}\n${injectedCode}`; | ||
if (typeof _sentry_default.getInitialProps === 'function') { | ||
_sentry_default.getInitialProps = withSentryGetInitialProps(_sentry_default.getInitialProps); | ||
} | ||
|
||
export default _sentry_default;`; | ||
} | ||
|
||
return outputFileContent; | ||
} else { | ||
// If none of the functions we want to wrap appears in the page's code, there's nothing to do. (Note: We do this as a | ||
// simple substring match (rather than waiting until we've parsed the code) because it's meant to be an | ||
// as-fast-as-possible fail-fast. It's possible for user code to pass this check, even if it contains none of the | ||
// functions in question, just by virtue of the correct string having been found, be it in a comment, as part of a | ||
// longer variable name, etc. That said, when we actually do the code manipulation we'll be working on the code's AST, | ||
// meaning we'll be able to differentiate between code we actually want to change and any false positives which might | ||
// come up here.) | ||
if (Object.keys(DATA_FETCHING_FUNCTIONS).every(functionName => !userCode.includes(functionName))) { | ||
return userCode; | ||
} | ||
|
||
const templatePath = path.resolve(__dirname, '../templates/dataFetchersLoaderTemplate.js'); | ||
// make sure the template is included when runing `webpack watch` | ||
this.addDependency(templatePath); | ||
|
||
const templateCode = fs.readFileSync(templatePath).toString(); | ||
|
||
const [modifiedUserCode, modifiedTemplateCode] = wrapFunctions( | ||
userCode, | ||
templateCode, | ||
// Relative path to the page we're currently processing, for use in error messages | ||
path.relative(projectDir, this.resourcePath), | ||
); | ||
|
||
// Fill in template placeholders | ||
let injectedCode = modifiedTemplateCode; | ||
for (const { placeholder, alias } of Object.values(DATA_FETCHING_FUNCTIONS)) { | ||
injectedCode = injectedCode.replace(new RegExp(placeholder, 'g'), alias); | ||
} | ||
|
||
return `${modifiedUserCode}\n${injectedCode}`; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.