Skip to content

Commit 8568ea5

Browse files
committed
add AST helper functions
1 parent 37b0a0a commit 8568ea5

File tree

1 file changed

+322
-0
lines changed
  • packages/nextjs/src/config/loaders

1 file changed

+322
-0
lines changed
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/* eslint-disable max-lines */
2+
import * as jscsTypes from 'jscodeshift';
3+
import { default as jscodeshiftDefault } from 'jscodeshift';
4+
5+
import { makeParser } from './parsers';
6+
7+
// In `jscodeshift`, the exports look like this:
8+
//
9+
// function core(...) { ... }
10+
// core.ABC = ...
11+
// core.XYZ = ...
12+
// module.exports = core
13+
//
14+
// In other words, when required/imported, the module is both a callable function and an object containing all sorts of
15+
// properties. Meanwhile, its TS export is a namespace continaing the types of all of the properties attached to `core`.
16+
// In order to use the types, we thus need to use `import *` syntax. But when we do that, Rollup only sees it as a
17+
// namespace, and will complain if we try to use it as a function. In order to get around this, we take advantage of the
18+
// fact that Rollup wraps imports in its own version of TS's `esModuleInterop` functions, aliasing the export to a
19+
// `default` property inside the export. (So, here, we basically end up with `core.default = core`.) When referenced
20+
// through that alias, `core` is correctly seen as callable by Rollup. Outside of a Rollup context, however, that
21+
// `default` alias doesn't exist. So, we try both and use whichever one is defined. (See
22+
// https://github.com/rollup/rollup/issues/1267.)
23+
const jscodeshiftNamespace = jscsTypes;
24+
const jscs = jscodeshiftDefault || jscodeshiftNamespace;
25+
26+
// These are types not in the TS sense, but in the instance-of-a-Type-class sense
27+
const {
28+
ExportSpecifier,
29+
Identifier,
30+
ImportSpecifier,
31+
MemberExpression,
32+
Node,
33+
ObjectExpression,
34+
ObjectPattern,
35+
Property,
36+
VariableDeclaration,
37+
VariableDeclarator,
38+
} = jscs;
39+
40+
type ASTNode = jscsTypes.ASTNode;
41+
export type AST<T = ASTNode> = jscsTypes.Collection<T>;
42+
// `parentPath` is on the prototype, but not included in the type for some reason. (`parent`, which is an instance
43+
// property referencing the same object as `parentPath`, is in the type, and we could use that instead. But the
44+
// `parentPath` name makes it clearer that said object is in fact a `NodePath`, not a `Node`, so we choose to use it
45+
// over `parent`, even if it means adding it to the type.)
46+
interface ASTPath<T = ASTNode> extends jscsTypes.ASTPath<T> {
47+
parentPath: ASTPath<ASTNode>;
48+
}
49+
type IdentifierNode = jscsTypes.Identifier;
50+
type ExportSpecifierNode = jscsTypes.ExportSpecifier;
51+
type VariableDeclarationNode = jscsTypes.VariableDeclaration;
52+
53+
/**
54+
* Create an AST based on the given code.
55+
*
56+
* @param code The code to convert to an AST.
57+
* @param isTS Flag indicating what parser to use.
58+
* @throws Parsing error if the code is unparsable
59+
* @returns The AST
60+
*/
61+
export function makeAST(code: string, isTS: boolean): AST {
62+
const parser = isTS ? makeParser('tsx') : makeParser('jsx');
63+
// If this errors, it will be caught in the calling function, where we know more information and can construct a
64+
// better warning message
65+
return jscs(code, { parser });
66+
}
67+
68+
/**
69+
* Find all nodes which represent Identifiers with the given name
70+
*
71+
* @param ast The code, in AST form
72+
* @param name The Identifier name to search for
73+
* @returns A collection of NodePaths pointing to any nodes which were found
74+
*/
75+
function findIdentifiers(ast: AST, name: string): AST<IdentifierNode> {
76+
const identifierFilter = function (path: ASTPath<IdentifierNode>): boolean {
77+
// Check that what we have is indeed an Identifier, and that the name matches
78+
//
79+
// Note: If we were being super precise about this, we'd also check the context in which the identifier is being
80+
// used, because there are some cases where we actually don't want to be renaming things (if the identifier is being
81+
// used to name a class property, for example). But the chances that someone is going to have a class property in a
82+
// nextjs page file with the same name as one of the canonical functions are slim to none, so for simplicity we can
83+
// stop filtering here. If this ever becomes a problem, more precise filter checks can be found in a comment at the
84+
// bottom of this file.
85+
return path.node.name === name;
86+
};
87+
88+
return ast.find(Identifier).filter(identifierFilter);
89+
}
90+
91+
/**
92+
* Find all nodes which are declarations of variables with the given name
93+
*
94+
* @param ast The code, in AST form
95+
* @param name The variable name to search for
96+
* @returns A collection of NodePaths pointing to any nodes which were found
97+
*/
98+
export function findDeclarations(ast: AST, name: string): AST<VariableDeclarationNode> {
99+
// Check for a structure of the form
100+
//
101+
// node: VariableDeclaration
102+
// \
103+
// declarations: VariableDeclarator[]
104+
// \
105+
// 0 : VariableDeclarator
106+
// \
107+
// id: Identifier
108+
// \
109+
// name: string
110+
//
111+
// where `name` matches the given name.
112+
const declarationFilter = function (path: ASTPath<VariableDeclarationNode>): boolean {
113+
return (
114+
path.node.declarations.length === 1 &&
115+
VariableDeclarator.check(path.node.declarations[0]) &&
116+
Identifier.check(path.node.declarations[0].id) &&
117+
path.node.declarations[0].id.name === name
118+
);
119+
};
120+
121+
return ast.find(VariableDeclaration).filter(declarationFilter);
122+
}
123+
124+
/**
125+
* Find all nodes which are exports of variables with the given name
126+
*
127+
* @param ast The code, in AST form
128+
* @param name The variable name to search for
129+
* @returns A collection of NodePaths pointing to any nodes which were found
130+
*/
131+
export function findExports(ast: AST, name: string): AST<ExportSpecifierNode> {
132+
const exportFilter = function (path: ASTPath<ExportSpecifierNode>): boolean {
133+
return ExportSpecifier.check(path.node) && path.node.exported.name === name;
134+
};
135+
136+
return ast.find(ExportSpecifier).filter(exportFilter);
137+
}
138+
139+
/**
140+
* Rename all identifiers with the given name, except in cases where it would break outside references.
141+
*
142+
* @param ast The AST representing the code
143+
* @param origName The name being replaced
144+
* @param newName The new name to use, if already chosen (one will be generated if not given)
145+
* @returns The new name assigned to the identifiers, or undefined if no identifiers were renamed
146+
*/
147+
export function renameIdentifiers(ast: AST, origName: string, newName?: string): string | undefined {
148+
const matchingNodes = findIdentifiers(ast, origName);
149+
150+
if (matchingNodes.length > 0) {
151+
// Find an available new name for the function by prefixing all references to it with an underscore (or a few
152+
// underscores, if that's what it takes to avoid a name collision).
153+
const alias = newName || findAvailibleAlias(ast, origName);
154+
matchingNodes.forEach(nodePath => {
155+
// Rename the node, except in cases where it might break an outside reference to it.
156+
maybeRenameNode(ast, nodePath, alias);
157+
});
158+
return alias;
159+
}
160+
161+
// technically redundant, but needed to keep TS happy
162+
return undefined;
163+
}
164+
165+
/**
166+
* Find an unused identifier name in the AST by repeatedly adding underscores to the beginning of the given original
167+
* name until we find one which hasn't already been taken.
168+
*
169+
* @param userAST The AST to search
170+
* @param origName The original name we want to alias
171+
* @returns
172+
*/
173+
function findAvailibleAlias(userAST: AST, origName: string): string {
174+
let foundAvailableName = false;
175+
let newName = origName;
176+
177+
while (!foundAvailableName) {
178+
// Prefix the original function name (or the last name we tried) with an underscore and search for identifiers with
179+
// the new name in the AST
180+
newName = `_${newName}`;
181+
const existingIdentifiers = findIdentifiers(userAST, newName);
182+
183+
// If we haven't found anything, we're good to go
184+
foundAvailableName = existingIdentifiers.length === 0;
185+
}
186+
187+
return newName;
188+
}
189+
190+
// When we're searching for and renaming the user's data-fetching functions, the general idea is to rename all
191+
// identifiers matching the function names, but there are a few things to watch out for:
192+
// - We can't rename any identifiers that refer to something outside of the module, because then we'd break the link
193+
// between the external thing and the module's reference to it. The two key examples of this are named imports and
194+
// property access in objects instantiated outside of the module.
195+
// - What nextjs cares about is just the identifier which gets exported, which may or may not be what it's called
196+
// locally. In other words, if we find something like `export { something as getServerSideProps }`, we have to
197+
// rename both `something` and `getServerSideProps`, the former so we can wrap it and the latter so as not to
198+
// conflict with the wrapped function of the same name we're planning to export.
199+
// - Shorthand object notation is a thing. Specifically, it's a thing which makes two separate identifiers appear as
200+
// one, even though they have separate functions and may need to be treated differently from one another. This shows
201+
// up not just in object literals but also when destructuring and in imports and exports.
202+
203+
function maybeRenameNode(ast: AST, identifierPath: ASTPath<IdentifierNode>, alias: string): void {
204+
const node = identifierPath.node;
205+
const parent = identifierPath.parentPath.node;
206+
const grandparent = identifierPath.parentPath.parentPath.node;
207+
208+
// In general we want to rename all nodes, unless we're in one of a few specific situations. (Anything which doesn't
209+
// get handled by one of these checks will be renamed at the end of this function.) In all of the scenarios below,
210+
// we'll use `gSSP` as our stand-in for any of `getServerSideProps`, `getStaticProps`, and `getStaticPaths`.
211+
212+
// Imports:
213+
//
214+
// - `import { gSSP } from 'yyy'`, which is equivalent (in AST terms) to `import { gSSP as gSSP } from 'yyy'`
215+
// - `import { xxx as gSSP } from 'yyy'`
216+
//
217+
// The `xxx as gSSP` corresponds to an ImportSpecifier, with `imported = xxx` and `local = gSSP`. In both of these
218+
// cases, we want to rename `local` (the thing on the right; that will happen below) but not `imported` (the thing on
219+
// the left).
220+
if (ImportSpecifier.check(parent)) {
221+
if (node === parent.imported) return;
222+
// The only other option is that `node === parent.local`. This will get renamed below.
223+
}
224+
225+
// Destructuring:
226+
//
227+
// - `const { gSSP } = yyy`, which is equivalent (in AST terms) to `const { gSSP:gSSP } = yyy`
228+
// - `const { xxx:gSSP } = yyy`
229+
//
230+
// This would come up if, for example, we were grabbing something from a namespace (`import * as yyy from 'zzz'; const
231+
// { xxx:gSSP } = yyy`). Here the `xxx:gSSP` corresponds to a Property (inside of an array inside of an ObjectPatten
232+
// inside of a VariableDeclarator), with `key = xxx` and `value = gSSP`. In both of these cases, we want to rename
233+
// `value` but not `key`. (Again here we're renaming the righthand thing but leaving the lefthand thing alone.)
234+
235+
// And
236+
// though it's unlikely to be as relevant here, it's worth noting that we see the exact same pattern when
237+
// instantiating an object literal - `{ xxx }` or `{ xxx: yyy }` - where we rename the value but not the key. The only
238+
// difference there is that it's an `ObjectExpression` rather than an `ObjectPattern`.)
239+
if (Property.check(parent) && ObjectPattern.check(grandparent)) {
240+
if (node === parent.key) return;
241+
// The only other option is that `node === parent.value`. This will get renamed below. When it does, the names of
242+
// `parent.key` and `parent.value` won't match (if they ever did), so we need to make sure to update `shorthand`.
243+
parent.shorthand = false;
244+
}
245+
246+
// Object literal instantiation:
247+
//
248+
// - `const xxx = { gSSP }`, which is equivalent (in AST terms) to `const xxx = { gSSP: gSSP }`
249+
// - `const xxx = { yyy: gSSP }`
250+
//
251+
// This is the same as destructuring in every way, with the exception that where there it was an `ObjectPattern`, here
252+
// it's an `ObjectExpression`.
253+
if (Property.check(parent) && ObjectExpression.check(grandparent)) {
254+
if (node === parent.key) return;
255+
// The only other option is that `node === parent.value`. This will get renamed below. When it does, the names of
256+
// `parent.key` and `parent.value` won't match (if they ever did), so we need to make sure to update `shorthand`.
257+
parent.shorthand = false;
258+
}
259+
260+
// Object property access:
261+
//
262+
// - xxx.yyy
263+
//
264+
// This is similar to destructuring (in that we we don't want to rename object keys), and would come up in similar
265+
// circumstances: `import * as xxx from 'abc'; const zzz = xxx.yyy`. In this case the `xxx.yyy` corresponds to a
266+
// `MemberExpression`, with `object = xxx` and `property = yyy`. (This is unlikely to be relevant in our case with
267+
// data-fetching functions, which is why none of the part of this example are `gSSP`. Nonetheless, good to be accurate
268+
// with these things.)
269+
if (MemberExpression.check(parent)) {
270+
if (node === parent.property) return;
271+
// The only other option is that `node === parent.object`. This will get renamed below.
272+
}
273+
274+
// Exports:
275+
//
276+
// - `export { gSSP }, which is equivalent (in AST terms) to `export { gSSP as gSSP }`
277+
// - `export { xxx as gSSP }`
278+
//
279+
// Similar to the `import` cases, here the `xxx as gSSP` corresponds to an `ExportSpecifier`, with `local = xxx` and
280+
// `exported = gSSP`. And as before, we want to change `local`, but this time there's a twist. (Two of them,
281+
// actually.)
282+
//
283+
// First, if we care about this ExportSpecifier at all, it's because it's the export of one of our data-fetching
284+
// functions, as in the example above. Because we want to export a replacement version of said function, we need to
285+
// rename `exported`, to prevent a name conflict. (This is different than what you'd expect from a simple "rename a
286+
// variable" algorithm, because in that case you normally wouldn't rename the thing which could be referred to outside
287+
// of the module.)
288+
//
289+
// Second, because need to wrap the object using its local name, we need to rename `local`. This tracks with how we
290+
// thought about `import` statements above, but is different from everything else we're doing in this function in that
291+
// it means we potentially need to rename something *not* already named `getServerSideProps`, `getStaticProps`, or
292+
// `getStaticPaths`, meaning we need to rename nodes outside of the collection upon which we're currently acting.
293+
if (ExportSpecifier.check(parent)) {
294+
// console.log(node);
295+
// debugger;
296+
if (parent.exported.name !== parent.local?.name && node === parent.exported) {
297+
const currentLocalName = parent.local?.name || '';
298+
renameIdentifiers(ast, currentLocalName, alias);
299+
}
300+
301+
// The only other options are that a) the names match, in which case both `local` and `exported` both have the name
302+
// of the function we're trying to wrap, and will get renamed below, or b) the names are different but `node` is
303+
// `local`, meaning this must be the second go-round of `renameIdentifiers`, where we're renaming everything with
304+
// the local name, not the name of our wrapped data-fetching function, in which case `node` (a.k.a. `local`) will
305+
// also get renamed below.
306+
}
307+
308+
// handle any node which hasn't gotten otherwise dealt with above
309+
node.name = alias;
310+
}
311+
312+
/**
313+
* Remove comments from all nodes in the given AST.
314+
*
315+
* Note: Comments are not nodes in and of themselves, but are instead attached to the nodes above and below them.
316+
*
317+
* @param ast The code, in AST form
318+
*/
319+
export function removeComments(ast: AST): void {
320+
const nodesWithComments = ast.find(Node).filter(nodePath => !!nodePath.node.comments);
321+
nodesWithComments.forEach(nodePath => (nodePath.node.comments = null));
322+
}

0 commit comments

Comments
 (0)