Skip to content

Commit 80a6d7e

Browse files
committed
add AST helper functions
1 parent 537d5f5 commit 80a6d7e

File tree

1 file changed

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

1 file changed

+219
-0
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import * as jscsTypes from 'jscodeshift';
2+
import { default as jscodeshiftDefault } from 'jscodeshift';
3+
4+
import { makeParser } from './parsers';
5+
6+
// In `jscodeshift`, the exports look like this:
7+
//
8+
// function core(...) { ... }
9+
// core.ABC = ...
10+
// core.XYZ = ...
11+
// module.exports = core
12+
//
13+
// In other words, when required/imported, the module is both a callable function and an object containing all sorts of
14+
// properties. Meanwhile, its TS export is a namespace continaing the types of all of the properties attached to `core`.
15+
// In order to use the types, we thus need to use `import *` syntax. But when we do that, Rollup only sees it as a
16+
// namespace, and will complain if we try to use it as a function. In order to get around this, we take advantage of the
17+
// fact that Rollup wraps imports in its own version of TS's `esModuleInterop` functions, aliasing the export to a
18+
// `default` property inside the export. (So, here, we basically end up with `core.default = core`.) When referenced
19+
// through that alias, `core` is correctly seen as callable by Rollup. Outside of a Rollup context, however, that
20+
// `default` alias doesn't exist. So, we try both and use whichever one is defined. (See
21+
// https://github.com/rollup/rollup/issues/1267.)
22+
const jscodeshiftNamespace = jscsTypes;
23+
const jscs = jscodeshiftDefault || jscodeshiftNamespace;
24+
25+
// These are types not in the TS sense, but in the instance-of-a-Type-class sense
26+
const { ExportSpecifier, Identifier, Node, VariableDeclaration, VariableDeclarator } = jscs;
27+
28+
export type AST<T = jscsTypes.ASTNode> = jscsTypes.Collection<T>;
29+
type ASTPath<T = jscsTypes.ASTNode> = jscsTypes.ASTPath<T>;
30+
type IdentifierNode = jscsTypes.Identifier;
31+
type ExportSpecifierNode = jscsTypes.ExportSpecifier;
32+
type VariableDeclarationNode = jscsTypes.VariableDeclaration;
33+
34+
/**
35+
* Create an AST based on the given code.
36+
*
37+
* @param code The code to convert to an AST.
38+
* @param isTS Flag indicating what parser to use.
39+
* @throws Parsing error if the code is unparsable
40+
* @returns The AST
41+
*/
42+
export function makeAST(code: string, isTS: boolean): AST {
43+
const parser = isTS ? makeParser('tsx') : makeParser('jsx');
44+
// If this errors, it will be caught in the calling function, where we know more information and can construct a
45+
// better warning message
46+
return jscs(code, { parser });
47+
}
48+
49+
/**
50+
* Find all nodes which represent Identifiers with the given name
51+
*
52+
* @param ast The code, in AST form
53+
* @param name The Identifier name to search for
54+
* @returns A collection of NodePaths pointing to any nodes which were found
55+
*/
56+
export function findIdentifiers(ast: AST, name: string): AST<IdentifierNode> {
57+
const identifierFilter = function (path: ASTPath<IdentifierNode>): boolean {
58+
// Check that what we have is indeed an Identifier, and that the name matches
59+
//
60+
// Note: If we were being super precise about this, we'd also check the context in which the identifier is being
61+
// used, because there are some cases where we actually don't want to be renaming things (if the identifier is being
62+
// used to name a class property, for example). But the chances that someone is going to have a class property in a
63+
// nextjs page file with the same name as one of the canonical functions are slim to none, so for simplicity we can
64+
// stop filtering here. If this ever becomes a problem, more precise filter checks can be found in a comment at the
65+
// bottom of this file.
66+
return path.node.name === name;
67+
};
68+
69+
return ast.find(Identifier).filter(identifierFilter);
70+
}
71+
72+
/**
73+
* Find all nodes which are declarations of variables with the given name
74+
*
75+
* @param ast The code, in AST form
76+
* @param name The variable name to search for
77+
* @returns A collection of NodePaths pointing to any nodes which were found
78+
*/
79+
export function findDeclarations(ast: AST, name: string): AST<VariableDeclarationNode> {
80+
// Check for a structure of the form
81+
//
82+
// node: VariableDeclaration
83+
// \
84+
// declarations: VariableDeclarator[]
85+
// \
86+
// 0 : VariableDeclarator
87+
// \
88+
// id: Identifier
89+
// \
90+
// name: string
91+
//
92+
// where `name` matches the given name.
93+
const declarationFilter = function (path: ASTPath<VariableDeclarationNode>): boolean {
94+
return (
95+
path.node.declarations.length === 1 &&
96+
VariableDeclarator.check(path.node.declarations[0]) &&
97+
Identifier.check(path.node.declarations[0].id) &&
98+
path.node.declarations[0].id.name === name
99+
);
100+
};
101+
102+
return ast.find(VariableDeclaration).filter(declarationFilter);
103+
}
104+
105+
/**
106+
* Find all nodes which are exports of variables with the given name
107+
*
108+
* @param ast The code, in AST form
109+
* @param name The variable name to search for
110+
* @returns A collection of NodePaths pointing to any nodes which were found
111+
*/
112+
export function findExports(ast: AST, name: string): AST<ExportSpecifierNode> {
113+
const exportFilter = function (path: ASTPath<ExportSpecifierNode>): boolean {
114+
return ExportSpecifier.check(path.node) && path.node.exported.name === name;
115+
};
116+
117+
return ast.find(ExportSpecifier).filter(exportFilter);
118+
}
119+
120+
/**
121+
* Remove comments from all nodes in the given AST.
122+
*
123+
* Note: Comments are not nodes in and of themselves, but are instead attached to the nodes above and below them.
124+
*
125+
* @param ast The code, in AST form
126+
*/
127+
export function removeComments(ast: AST): void {
128+
const nodesWithComments = ast.find(Node).filter(path => !!path.node.comments);
129+
nodesWithComments.forEach(path => (path.node.comments = null));
130+
}
131+
132+
/**
133+
* Find an unused identifier name in the AST by repeatedly adding underscores to the beginning of the given original
134+
* name until we find one which hasn't already been taken.
135+
*
136+
* @param userAST The AST to search
137+
* @param origName The original name we want to alias
138+
* @returns
139+
*/
140+
export function findAvailibleAlias(userAST: AST, origName: string): string {
141+
let foundAvailableName = false;
142+
let newName = origName;
143+
144+
while (!foundAvailableName) {
145+
// Prefix the original function name (or the last name we tried) with an underscore and search for identifiers with
146+
// the new name in the AST
147+
newName = `_${newName}`;
148+
const existingIdentifiers = findIdentifiers(userAST, newName);
149+
150+
// If we haven't found anything, we're good to go
151+
foundAvailableName = existingIdentifiers.length === 0;
152+
}
153+
154+
return newName;
155+
}
156+
157+
/**
158+
* More precise version of `identifierFilter`, used in `findIdentifiers`, which accounts for context. See note in
159+
* `findIdentifiers` above.
160+
*/
161+
162+
// const {
163+
// AssignmentExpression,
164+
// CallExpression,
165+
// ExportSpecifier,
166+
// FunctionDeclaration,
167+
// Identifier,
168+
// MemberExpression,
169+
// Node,
170+
// Property,
171+
// ReturnStatement,
172+
// VariableDeclaration,
173+
// VariableDeclarator,
174+
// } = jscs;
175+
//
176+
// const identifierFilter = function (path: ASTPath<Identifier>): boolean {
177+
// const node = path.node;
178+
// const parentPath = path.parent as ASTPath;
179+
// const parent = parentPath.node;
180+
//
181+
// const hasCorrectName = node.name === name;
182+
//
183+
// // Check that the identifier is being used in a valid context, one in which we do in fact want to replace it.
184+
// //
185+
// // Note: There are a million ways identifiers can be used - this is just a subset, but it should hit 99% of cases.
186+
// // If anyone every files an issue because we're doing an incomplete job of transforming their code, get a
187+
// // representative sample from them and throw it into https://astexplorer.net/ or
188+
// // https://rajasegar.github.io/ast-finder/ (making sure in either case to set the parser to `recast`) to figure out
189+
// // what to add below. (Find the `Identifier` node and note its parent's `type` value and the name of the key under
190+
// // which it lives.) Note that neither tool seems to be able to handle the `export` keyword for some reason, but for
191+
// // anything other than the case already included below, `ExportSpecifier` will be at least the grandparent; given
192+
// // that we only care about recognizing the parent, we can just remove `export` from the sample code and it won't
193+
// // make any difference to the part we care about.
194+
// //
195+
// // In all of the examples in the comments below, the identifer we're interested in is `someFunc`.
196+
// const contextIsValid =
197+
// // `export const someFunc = ...` or `const someFunc = ...` or `let someFunc`
198+
// (VariableDeclarator.check(parent) && parent.id === node) ||
199+
// // `export { someFunc }` or `export { someOtherFunc as someFunc }`
200+
// (ExportSpecifier.check(parent) && parent.exported === node) ||
201+
// // `export function someFunc() { ... }` or `function someFunc() { ... }`
202+
// (FunctionDeclaration.check(parent) && parent.id === node) ||
203+
// // `someFunc = ...`
204+
// (AssignmentExpression.check(parent) && parent.left === node) ||
205+
// // `someVariable = someFunc`
206+
// (AssignmentExpression.check(parent) && parent.right === node) ||
207+
// // `const someVariable = someFunc`
208+
// (VariableDeclarator.check(parent) && parent.init === node) ||
209+
// // `someFunc.someProperty`
210+
// (MemberExpression.check(parent) && parent.object === node) ||
211+
// // `{ someProperty: someFunc }`
212+
// (Property.check(parent) && parent.value === node) ||
213+
// // `someOtherFunc(someFunc)`
214+
// (CallExpression.check(parent) && parent.arguments.includes(node)) ||
215+
// // `return someFunc`
216+
// (ReturnStatement.check(parent) && parent.argument === node);
217+
//
218+
// return hasCorrectName && contextIsValid;
219+
// };

0 commit comments

Comments
 (0)