Skip to content

Commit 56e076a

Browse files
committed
add loader for wrapping data-fetching functions
1 parent 0757e91 commit 56e076a

File tree

4 files changed

+114
-5
lines changed

4 files changed

+114
-5
lines changed

packages/nextjs/rollup.npm.config.js

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

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