Skip to content

Commit 0ed650d

Browse files
committed
add loader for wrapping data-fetching functions
1 parent 7ea5bd6 commit 0ed650d

File tree

4 files changed

+124
-5
lines changed

4 files changed

+124
-5
lines changed

packages/nextjs/rollup.npm.config.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,12 @@ export default [
3636
),
3737
...makeNPMConfigVariants(
3838
makeBaseNPMConfig({
39-
entrypoints: ['src/config/loaders/prefixLoader.ts'],
39+
entrypoints: ['src/config/loaders/index.ts'],
4040

4141
packageSpecificConfig: {
4242
output: {
4343
// make it so Rollup calms down about the fact that we're doing `export { loader as default }`
44-
exports: 'default',
45-
46-
// preserve the original file structure (i.e., so that everything is still relative to `src`)
47-
entryFileNames: 'config/loaders/[name].js',
44+
exports: 'named',
4845
},
4946
},
5047
}),
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { logger } from '@sentry/utils';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
import { isESM } from '../../utils/isESM';
6+
import {
7+
AST,
8+
findAvailibleAlias,
9+
findDeclarations,
10+
findExports,
11+
findIdentifiers,
12+
makeAST,
13+
removeComments,
14+
} from './ast';
15+
import { LoaderThis } from './types';
16+
17+
const DATA_FETCHING_FUNCTIONS = ['getServerSideProps', 'getStaticProps', 'getStaticPaths'];
18+
19+
type LoaderOptions = {
20+
projectDir: string;
21+
};
22+
23+
/**
24+
* Find any data-fetching functions the user's code contains and rename them to prevent clashes, then whittle the
25+
* template exporting wrapped versions instead down to only the functions found.
26+
*
27+
* @param userCode The source code of the current page file
28+
* @param templateCode The source code of the full template, including all functions
29+
* @param filepath The path to the current pagefile, within the project directory
30+
* @returns A tuple of modified user and template code
31+
*/
32+
function wrapFunctions(userCode: string, templateCode: string, filepath: string): string[] {
33+
let userAST: AST, templateAST: AST;
34+
const isTS = new RegExp('\\.tsx?$').test(filepath);
35+
36+
try {
37+
userAST = makeAST(userCode, isTS);
38+
templateAST = makeAST(templateCode, false);
39+
} catch (err) {
40+
logger.warn(`Couldn't add Sentry to ${filepath} because there was a parsing error: ${err}`);
41+
// Replace the template code with an empty string, so in the end the user code is untouched
42+
return [userCode, ''];
43+
}
44+
45+
// Comments are useful to have in the template for anyone reading it, but don't make sense to be injected into user
46+
// code, because they're about the template-i-ness of the template, not the code itself
47+
removeComments(templateAST);
48+
49+
for (const fnName of DATA_FETCHING_FUNCTIONS) {
50+
const matchingNodes = findIdentifiers(userAST, fnName);
51+
52+
// If the current function exists in a user's code, prefix all references to it with an underscore, so as not to
53+
// conflict with the wrapped version we're going to create
54+
if (matchingNodes.length > 0) {
55+
matchingNodes.forEach(nodePath => (nodePath.node.name = findAvailibleAlias(userAST, fnName)));
56+
}
57+
58+
// Otherwise, if the current function doesn't exist anywhere in the user's code, delete the code in the template
59+
// wrapping that function
60+
//
61+
// Note: We start with all of the possible wrapper lines in the template and delete the ones we don't need (rather
62+
// than starting with none and adding in the ones we do need) because it allows them to live in our souce code as
63+
// *code*. If we added them in, they'd have to be strings containing code, and we'd lose all of the benefits of
64+
// syntax highlighting, linting, etc.
65+
else {
66+
// We have to look for declarations and exports separately because when we build the SDK, Rollup turns
67+
// export const XXX = ...
68+
// into
69+
// const XXX = ...
70+
// export { XXX }
71+
findExports(templateAST, fnName).remove();
72+
findDeclarations(templateAST, fnName).remove();
73+
}
74+
}
75+
76+
return [userAST.toSource(), templateAST.toSource()];
77+
}
78+
79+
/**
80+
* Wrap `getStaticPaths`, `getStaticProps`, and `getServerSideProps` (if they exist) in the given page code
81+
*/
82+
function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>, userCode: string): string {
83+
// We know one or the other will be defined, depending on the version of webpack being used
84+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
85+
const { projectDir } = this.getOptions ? this.getOptions() : this.query!;
86+
87+
// For now this loader only works for ESM code
88+
// TODO: Can you even write nextjs pages in CJS?
89+
if (!isESM(userCode)) {
90+
return userCode;
91+
}
92+
93+
// 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
94+
// simple substring match (rather than parsing the code yet) because it's meant to be an as-fast-as-possible
95+
// fail-fast. It's possible for user code to pass this check, even if it contains none of the functions in question,
96+
// just by virtue of the correct string having been found, be it in a comment, as part of a longer variable name, etc.
97+
// That said, when we actually do the code manipulation we work on the code's AST, meaning we'll be able to
98+
// differentiate between code we actually want to change and any false positives which might come up here.)
99+
if (DATA_FETCHING_FUNCTIONS.every(functionName => !userCode.includes(functionName))) {
100+
return userCode;
101+
}
102+
103+
const templatePath = path.resolve(__dirname, '../templates/dataFetchersLoaderTemplate.js');
104+
// make sure the template is included when runing `webpack watch`
105+
this.addDependency(templatePath);
106+
107+
const templateCode = fs.readFileSync(templatePath).toString();
108+
109+
const [modifiedUserCode, injectedCode] = wrapFunctions(
110+
userCode,
111+
templateCode,
112+
path.relative(projectDir, this.resourcePath),
113+
);
114+
return `${modifiedUserCode}\n${injectedCode}`;
115+
}
116+
117+
export { wrapDataFetchersLoader as default };
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as prefixLoader } from './prefixLoader';
2+
export { default as dataFetchersLoader } from './dataFetchersLoader';

packages/nextjs/src/config/loaders/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// TODO Use real webpack types
22
export type LoaderThis<Options> = {
3+
// Path to the file being loaded
4+
resourcePath: string;
5+
36
// Loader options in Webpack 4
47
query?: Options;
58
// Loader options in Webpack 5

0 commit comments

Comments
 (0)