Skip to content

Commit f588a2b

Browse files
committed
working, needs formatting, consolidation
1 parent d5298b5 commit f588a2b

17 files changed

+833
-16
lines changed

packages/nextjs/.eslintrc.js

+8
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,12 @@ module.exports = {
1111
rules: {
1212
'@sentry-internal/sdk/no-async-await': 'off',
1313
},
14+
overrides: [
15+
{
16+
files: ['scripts/**/*.ts'],
17+
parserOptions: {
18+
project: ['../../tsconfig.dev.json'],
19+
},
20+
},
21+
],
1422
};

packages/nextjs/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
"@sentry/types": "7.8.0",
2727
"@sentry/utils": "7.8.0",
2828
"@sentry/webpack-plugin": "1.19.0",
29+
"jscodeshift": "^0.13.1",
2930
"tslib": "^1.9.3"
3031
},
3132
"devDependencies": {
33+
"@types/jscodeshift": "^0.11.5",
3234
"@types/webpack": "^4.41.31",
35+
"ast-types": "^0.14.2",
3336
"next": "10.1.3"
3437
},
3538
"peerDependencies": {
@@ -45,11 +48,11 @@
4548
"scripts": {
4649
"build": "run-p build:rollup build:types",
4750
"build:dev": "run-s build",
48-
"build:rollup": "rollup -c rollup.npm.config.js",
51+
"build:rollup": "ts-node scripts/buildRollup.ts",
4952
"build:types": "tsc -p tsconfig.types.json",
5053
"build:watch": "run-p build:rollup:watch build:types:watch",
5154
"build:dev:watch": "run-s build:watch",
52-
"build:rollup:watch": "rollup -c rollup.npm.config.js --watch",
55+
"build:rollup:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts",
5356
"build:types:watch": "tsc -p tsconfig.types.json --watch",
5457
"build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build",
5558
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular --exclude 'config/types\\.ts' src/index.server.ts # see https://github.com/pahen/madge/issues/306",

packages/nextjs/rollup.npm.config.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ export default [
1414
),
1515
...makeNPMConfigVariants(
1616
makeBaseNPMConfig({
17-
entrypoints: ['src/config/prefixLoaderTemplate.ts'],
17+
entrypoints: [
18+
'src/config/templates/prefixLoaderTemplate.ts',
19+
'src/config/templates/dataFetchersLoaderTemplate.ts',
20+
],
1821

1922
packageSpecificConfig: {
2023
output: {
2124
// preserve the original file structure (i.e., so that everything is still relative to `src`)
22-
entryFileNames: 'config/[name].js',
25+
entryFileNames: 'config/templates/[name].js',
2326

2427
// this is going to be add-on code, so it doesn't need the trappings of a full module (and in fact actively
2528
// shouldn't have them, lest they muck with the module to which we're adding it)
@@ -31,15 +34,15 @@ export default [
3134
),
3235
...makeNPMConfigVariants(
3336
makeBaseNPMConfig({
34-
entrypoints: ['src/config/prefixLoader.ts'],
37+
entrypoints: ['src/config/loaders/index.ts'],
3538

3639
packageSpecificConfig: {
3740
output: {
3841
// make it so Rollup calms down about the fact that we're doing `export { loader as default }`
39-
exports: 'default',
42+
exports: 'named',
4043

4144
// preserve the original file structure (i.e., so that everything is still relative to `src`)
42-
entryFileNames: 'config/[name].js',
45+
entryFileNames: 'config/loaders/[name].js',
4346
},
4447
},
4548
}),
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as childProcess from 'child_process';
2+
import * as fs from 'fs';
3+
4+
/**
5+
* Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current
6+
* process. Returns contents of `stdout`.
7+
*/
8+
function run(cmd: string, options?: childProcess.ExecSyncOptions): string | Buffer {
9+
return childProcess.execSync(cmd, { stdio: 'inherit', ...options });
10+
}
11+
12+
run('yarn rollup -c rollup.npm.config.js');
13+
14+
// Because we might be injecting into both CJS and ESM files, we need both versions of the template accessible in each
15+
// build folder. First we rename the existing built files to denote which type they are, then we copy the CJS one into
16+
// the ESM directory and vice-versa.
17+
fs.renameSync(
18+
'build/cjs/config/templates/dataFetchersLoaderTemplate.js',
19+
'build/cjs/config/templates/dataFetchersLoaderTemplate.cjs.js',
20+
);
21+
fs.renameSync(
22+
'build/esm/config/templates/dataFetchersLoaderTemplate.js',
23+
'build/esm/config/templates/dataFetchersLoaderTemplate.esm.js',
24+
);
25+
fs.copyFileSync(
26+
'build/cjs/config/templates/dataFetchersLoaderTemplate.cjs.js',
27+
'build/esm/config/templates/dataFetchersLoaderTemplate.cjs.js',
28+
);
29+
fs.copyFileSync(
30+
'build/esm/config/templates/dataFetchersLoaderTemplate.esm.js',
31+
'build/cjs/config/templates/dataFetchersLoaderTemplate.esm.js',
32+
);
+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// For some reason, the `ASTNode` type from `jscodeshift` doesn't match up with the `ASTNode` type expected by
2+
// `Collection.filter` functions, which are also from `jscodeshift`, so... it should work? But in `jscodeshift` there
3+
// seems to be a mismatch between the `ASTNode` type from `ast-types` and the `ASTNode` type from `ast-types/lib/types`,
4+
// wherein the former is exported but filter functions require the latter. Needs more investigation. In the meantime, we
5+
// can just import directly from `ast-types`.
6+
import type { ASTNode as ASTNodeType } from 'ast-types';
7+
import * as jscsTypes from 'jscodeshift';
8+
import { default as jscodeshiftDefault } from 'jscodeshift';
9+
10+
// In `jscodeshift`, the exports look like this:
11+
//
12+
// function core(...) { ... }
13+
// core.ABC = ...
14+
// core.XYZ = ...
15+
// module.exports = core
16+
//
17+
// In other words, when required/imported, the module is both a callable function and an object containing all sorts of
18+
// properties. Meanwhile, its TS export is a namespace continaing the types of all of the properties attached to `core`.
19+
// In order to use the types, we thus need to use `import *` syntax. But when we do that, Rollup only sees it as a
20+
// namespace, and will complain if we try to use it as a function. In order to get around this, we take advantage of the
21+
// fact that Rollup wraps imports in its own version of TS's `esModuleInterop` functions, aliasing the export to a
22+
// `default` property inside the export. (So, here, we basically end up with `core.default = core`.) When referenced
23+
// through that alias, `core` is correctly seen as callable by Rollup. Outside of a Rollup context, however, that
24+
// `default` alias doesn't exist. So, we try both and use whichever one is defined. (See https://github.com/rollup/rollup/issues/1267.)
25+
const jscodeshiftNamespace = jscsTypes;
26+
const jscs = jscodeshiftDefault || jscodeshiftNamespace;
27+
28+
const {
29+
ExportSpecifier,
30+
Identifier,
31+
Node,
32+
VariableDeclaration,
33+
VariableDeclarator,
34+
} = jscs;
35+
36+
type ASTFilterFunction = (path: jscsTypes.ASTPath<ASTNodeType>) => boolean;
37+
38+
/**
39+
* Create a filter for nodes representing Identifiers with the given name
40+
*
41+
* @param name The variable name to filter on
42+
* @returns A filter function with the name baked in
43+
*/
44+
function makeIdentifierFilter(name: string): ASTFilterFunction {
45+
const identifierFilter = function (nodePath: jscsTypes.ASTPath<ASTNodeType>): boolean {
46+
// Check that what we have is indeed an Identifier, and that the name matches
47+
//
48+
// Note: If we were being super precise about this, we'd also check the context in which the identifier is being
49+
// used, because there are some cases where we actually don't want to be renaming things (if the identifier is being
50+
// used to name a class property, for example). But the chances that someone is going to have a class property in a
51+
// nextjs page file with the same name as one of the canonical functions are slim to none, so for simplicity we can
52+
// stop filtering here. If this ever becomes a problem, more precise filter checks can be found in a comment at the
53+
// bottom of this file.
54+
return Identifier.check(nodePath.node) && nodePath.node.name === name;
55+
};
56+
57+
return identifierFilter;
58+
}
59+
60+
/**
61+
* Create a filter for nodes declaring variables with the given name
62+
*
63+
* @param name The variable name to filter on
64+
* @returns A filter function with the name baked in
65+
*/
66+
function makeDeclarationFilter(name: string): ASTFilterFunction {
67+
// Check for a structure of the form
68+
//
69+
// node: VariableDeclaration
70+
// \
71+
// declarations: VariableDeclarator[]
72+
// \
73+
// 0 : VariableDeclarator
74+
// \
75+
// id: Identifier
76+
// \
77+
// name: string
78+
//
79+
// where `name` matches the given name.
80+
const declarationFilter = function (path: jscsTypes.ASTPath<ASTNodeType>): boolean {
81+
return (
82+
VariableDeclaration.check(path.node) &&
83+
path.node.declarations.length === 1 &&
84+
VariableDeclarator.check(path.node.declarations[0]) &&
85+
Identifier.check(path.node.declarations[0].id) &&
86+
path.node.declarations[0].id.name === name
87+
);
88+
};
89+
90+
return declarationFilter;
91+
}
92+
93+
/**
94+
* Create a filter for nodes representing exportss with the given name
95+
*
96+
* @param name The variable name to filter on
97+
* @returns A filter function with the name baked in
98+
*/
99+
function makeExportFilter(name: string): ASTFilterFunction {
100+
const exportFilter = function (path: jscsTypes.ASTPath<ASTNodeType>): boolean {
101+
return ExportSpecifier.check(path.node) && path.node.exported.name === name;
102+
};
103+
104+
return exportFilter;
105+
}
106+
107+
/**
108+
* Find all nodes which are representing Identifiers with the given name
109+
*
110+
* @param ast The code, in AST form
111+
* @param name The Identifier name to search for
112+
* @returns A collection of NodePaths pointing to any nodes which were found
113+
*/
114+
export function findIdentifiers(ast: jscsTypes.Collection, name: string): jscsTypes.Collection {
115+
return findNodes(ast, 'Identifier', name);
116+
}
117+
118+
/**
119+
* Find all nodes which are declarations of variables with the given name
120+
*
121+
* @param ast The code, in AST form
122+
* @param name The variable name to search for
123+
* @returns A collection of NodePaths pointing to any nodes which were found
124+
*/
125+
export function findDeclarations(ast: jscsTypes.Collection, name: string): jscsTypes.Collection {
126+
return findNodes(ast, 'VariableDeclaration', name);
127+
}
128+
129+
/**
130+
* Find all nodes which are exports of variables with the given name
131+
*
132+
* @param ast The code, in AST form
133+
* @param name The variable name to search for
134+
* @returns A collection of NodePaths pointing to any nodes which were found
135+
*/
136+
export function findExports(ast: jscsTypes.Collection, name: string): jscsTypes.Collection {
137+
return findNodes(ast, 'ExportSpecifier', name);
138+
}
139+
140+
/**
141+
* Remove comments from all nodes in the given AST.
142+
*
143+
* Note: Comments are not nodes in and of themselves, but are instead attached to the nodes above and below them.
144+
*
145+
* @param ast The code, in AST form
146+
*/
147+
export function removeComments(ast: jscsTypes.Collection): void {
148+
const nodesWithComments = ast.find(Node).filter(
149+
path =>
150+
!!(
151+
path.node.comments
152+
),
153+
);
154+
nodesWithComments.forEach(path => (path.node.comments = null));
155+
}
156+
157+
/**
158+
* Find all nodes of the given type with the given name
159+
*
160+
* @param ast The code, in AST form
161+
* @param nodeType The type of node for which to search
162+
* @param name The identifier which should be associated with the found nodes
163+
* @returns A collection of NodePaths pointing to any nodes which were found
164+
*/
165+
function findNodes(ast: jscsTypes.Collection, nodeType: string, name: string): jscsTypes.Collection {
166+
const filterFactories: { [key: string]: (name: string) => ASTFilterFunction } = {
167+
VariableDeclaration: makeDeclarationFilter,
168+
Identifier: makeIdentifierFilter,
169+
ExportSpecifier: makeExportFilter,
170+
};
171+
const filter = filterFactories[nodeType](name);
172+
return ast.find(Node).filter(filter);
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { logger } from '@sentry/utils';
2+
import * as fs from 'fs';
3+
import * as jscsTypes from 'jscodeshift';
4+
import { default as jscodeshiftDefault } from 'jscodeshift';
5+
import * as path from 'path';
6+
7+
import { findDeclarations, findExports, findIdentifiers, removeComments } from './ast';
8+
9+
// In `jscodeshift`, the exports look like this:
10+
//
11+
// function core(...) { ... }
12+
// core.ABC = ...
13+
// core.XYZ = ...
14+
// module.exports = core
15+
//
16+
// In other words, when required/imported, the module is both a callable function and an object containing all sorts of
17+
// properties. Meanwhile, its TS export is a namespace continaing the types of all of the properties attached to `core`.
18+
// In order to use the types, we thus need to use `import *` syntax. But when we do that, Rollup only sees it as a
19+
// namespace, and will complain if we try to use it as a function. In order to get around this, we take advantage of the
20+
// fact that Rollup wraps imports in its own version of TS's `esModuleInterop` functions, aliasing the export to a
21+
// `default` property inside the export. (So, here, we basically end up with `core.default = core`.) When referenced
22+
// through that alias, `core` is correctly seen as callable by Rollup. Outside of a Rollup context, however, that
23+
// `default` alias doesn't exist. So, we try both and use whichever one is defined. (See https://github.com/rollup/rollup/issues/1267.)
24+
const jscodeshiftNamespace = jscsTypes;
25+
const jscs = jscodeshiftDefault || jscodeshiftNamespace;
26+
27+
const DATA_FETCHING_FUNCTIONS = ['getServerSideProps', 'getStaticProps', 'getStaticPaths'];
28+
29+
type LoaderThis = {
30+
// Path to the file being loaded
31+
resourcePath: string;
32+
addDependency: (filepath: string) => void;
33+
};
34+
35+
function wrapFunctions(userCode: string, templateCode: string): string[] {
36+
const userAST = jscs(userCode);
37+
const templateAST = jscs(templateCode);
38+
39+
// Comments are useful to have in the template for anyone reading it, but don't make sense to be injected into user
40+
// code, because they're about the template-i-ness of the template, not the code itself
41+
removeComments(templateAST);
42+
43+
for (const fnName of DATA_FETCHING_FUNCTIONS) {
44+
const matchingNodes = findIdentifiers(userAST, fnName) as jscsTypes.Collection<jscsTypes.Identifier>;
45+
46+
// If the current function exists in a user's code, prefix all references to it with an underscore, so as not to
47+
// conflict with the wrapped version we're going to create
48+
if (matchingNodes.length > 0) {
49+
matchingNodes.forEach(nodePath => (nodePath.node.name = `_${fnName}`));
50+
}
51+
52+
// Otherwise, if the current function doesn't exist anywhere in the user's code, delete the code in the template
53+
// wrapping that function
54+
//
55+
// Note: We start with all of the possible wrapper lines in the template and delete the ones we don't need (rather
56+
// than starting with none and adding in the ones we do need) because it allows them to live in our souce code as
57+
// *code*. If we added them in, they'd have to be strings containing code, and we'd lose all of the benefits of
58+
// syntax highlighting, linting, etc.
59+
else {
60+
// We have to look for declarations and exports separately because when we build the SDK, Rollup turns
61+
// export const XXX = ...
62+
// into
63+
// const XXX = ...
64+
// export { XXX }
65+
findExports(templateAST, fnName).remove();
66+
findDeclarations(templateAST, fnName).remove();
67+
}
68+
}
69+
70+
return [userAST.toSource(), templateAST.toSource()];
71+
}
72+
73+
/**
74+
* Wrap `getStaticPaths`, `getStaticProps`, and `getServerSideProps` (if they exist) in the given page code
75+
*/
76+
function wrapDataFetchersLoader(this: LoaderThis, userCode: string): string {
77+
// If none of the functions we want to wrap appear in the page's code, there's nothing to do
78+
if (DATA_FETCHING_FUNCTIONS.every(functionName => !userCode.includes(functionName))) {
79+
return userCode;
80+
}
81+
82+
const templatePath = path.resolve(__dirname, '../templates/dataFetchersLoaderTemplate.esm.js');
83+
this.addDependency(templatePath);
84+
85+
let templateCode = fs.readFileSync(templatePath).toString();
86+
const [modifiedUserCode, injectedCode] = wrapFunctions(userCode, templateCode);
87+
return `${modifiedUserCode}\n${injectedCode}`;
88+
}
89+
90+
export { wrapDataFetchersLoader as default };
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/prefixLoader.ts renamed to packages/nextjs/src/config/loaders/prefixLoader.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function prefixLoader(this: LoaderThis, userCode: string): string {
2121
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2222
const { distDir } = this.getOptions ? this.getOptions() : this.query!;
2323

24-
const templatePath = path.resolve(__dirname, 'prefixLoaderTemplate.js');
24+
const templatePath = path.resolve(__dirname, '../templates/prefixLoaderTemplate.js');
2525
this.addDependency(templatePath);
2626

2727
// Fill in the placeholder

0 commit comments

Comments
 (0)