Skip to content
This repository was archived by the owner on Oct 10, 2022. It is now read-only.

Commit 0922b9e

Browse files
committed
feat: extract implementation from ZISI
1 parent 969dfa1 commit 0922b9e

File tree

9 files changed

+347
-12
lines changed

9 files changed

+347
-12
lines changed

.eslintrc.cjs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,23 @@ const { overrides } = require('@netlify/eslint-config-node/.eslintrc_esm.cjs')
44

55
module.exports = {
66
extends: '@netlify/eslint-config-node/.eslintrc_esm.cjs',
7-
overrides: [...overrides],
7+
rules: {
8+
'n/no-missing-import': 'off',
9+
// This is disabled because TypeScript transpiles some features currently
10+
// unsupported by Node 12, i.e. optional chaining
11+
// TODO: re-enable after dropping support for Node 12
12+
'n/no-unsupported-features/es-syntax': 'off',
13+
},
14+
overrides: [
15+
...overrides,
16+
{
17+
files: '*.ts',
18+
rules: {
19+
// Pure ES modules with TypeScript require using `.js` instead of `.ts`
20+
// in imports
21+
'import/extensions': 'off',
22+
'import/no-namespace': 'off',
23+
},
24+
},
25+
],
826
}

package-lock.json

Lines changed: 7 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,9 @@
5757
},
5858
"engines": {
5959
"node": "^12.20.0 || ^14.14.0 || >=16.0.0"
60+
},
61+
"dependencies": {
62+
"@babel/parser": "^7.18.6",
63+
"@babel/types": "^7.18.7"
6064
}
6165
}

src/exports.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { CallExpression, Statement } from '@babel/types'
2+
3+
import { isModuleExports } from './helpers.js'
4+
5+
import type { ISCExport } from './index.js'
6+
7+
// Finds the main handler export in an AST.
8+
export const getMainExport = (nodes: Statement[]) => {
9+
let handlerExport: ISCExport[] = []
10+
11+
nodes.find((node) => {
12+
const esmExports = getMainExportFromESM(node)
13+
14+
if (esmExports.length !== 0) {
15+
handlerExport = esmExports
16+
17+
return true
18+
}
19+
20+
const cjsExports = getMainExportFromCJS(node)
21+
22+
if (cjsExports.length !== 0) {
23+
handlerExport = cjsExports
24+
25+
return true
26+
}
27+
28+
return false
29+
})
30+
31+
return handlerExport
32+
}
33+
34+
// Finds the main handler export in a CJS AST.
35+
const getMainExportFromCJS = (node: Statement) => {
36+
const handlerPaths = [
37+
['module', 'exports', 'handler'],
38+
['exports', 'handler'],
39+
]
40+
41+
return handlerPaths.flatMap((handlerPath) => {
42+
if (!isModuleExports(node, handlerPath) || node.expression.right.type !== 'CallExpression') {
43+
return []
44+
}
45+
46+
return getExportsFromCallExpression(node.expression.right)
47+
})
48+
}
49+
50+
// Finds the main handler export in an ESM AST.
51+
// eslint-disable-next-line complexity
52+
const getMainExportFromESM = (node: Statement) => {
53+
if (node.type !== 'ExportNamedDeclaration' || node.exportKind !== 'value') {
54+
return []
55+
}
56+
57+
const { declaration } = node
58+
59+
if (!declaration || declaration.type !== 'VariableDeclaration') {
60+
return []
61+
}
62+
63+
const handlerDeclaration = declaration.declarations.find((childDeclaration) => {
64+
const { id, type } = childDeclaration
65+
66+
return type === 'VariableDeclarator' && id.type === 'Identifier' && id.name === 'handler'
67+
})
68+
69+
if (handlerDeclaration?.init?.type !== 'CallExpression') {
70+
return []
71+
}
72+
73+
const exports = getExportsFromCallExpression(handlerDeclaration.init)
74+
75+
return exports
76+
}
77+
78+
const getExportsFromCallExpression = (node: CallExpression) => {
79+
const { arguments: args, callee } = node
80+
81+
if (callee.type !== 'Identifier') {
82+
return []
83+
}
84+
85+
const exports = [{ local: callee.name, args }]
86+
87+
return exports
88+
}

src/helpers.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
AssignmentExpression,
3+
Expression,
4+
ExpressionStatement,
5+
ImportDeclaration,
6+
Statement,
7+
StringLiteral,
8+
V8IntrinsicIdentifier,
9+
} from '@babel/types'
10+
11+
// eslint-disable-next-line complexity
12+
const isDotExpression = (node: Expression, expression: string[]): boolean => {
13+
if (node.type !== 'MemberExpression') {
14+
return false
15+
}
16+
17+
const object = expression.slice(0, -1)
18+
const [property] = expression.slice(-1)
19+
20+
if (node.property.type !== 'Identifier' || node.property.name !== property) {
21+
return false
22+
}
23+
24+
if (object.length > 1) {
25+
return isDotExpression(node.object, object)
26+
}
27+
28+
return node.object.type === 'Identifier' && object[0] === node.object.name && property === node.property.name
29+
}
30+
31+
export const isImport = (node: Statement, importPath: string): node is ImportDeclaration =>
32+
node.type === 'ImportDeclaration' && node.source.value === importPath
33+
34+
export const isModuleExports = (
35+
node: Statement,
36+
dotExpression = ['module', 'exports'],
37+
): node is ExpressionStatement & { expression: AssignmentExpression } =>
38+
node.type === 'ExpressionStatement' &&
39+
node.expression.type === 'AssignmentExpression' &&
40+
node.expression.left.type === 'MemberExpression' &&
41+
isDotExpression(node.expression.left, dotExpression)
42+
43+
export const isRequire = (node: Expression | undefined | null, requirePath: string) => {
44+
if (!node || node.type !== 'CallExpression') {
45+
return false
46+
}
47+
48+
const { arguments: args, callee } = node
49+
const isRequiredModule = args[0]?.type === 'StringLiteral' && isRequirePath(args[0], requirePath)
50+
51+
return isRequireCall(callee) && isRequiredModule
52+
}
53+
54+
const isRequireCall = (node: Expression | V8IntrinsicIdentifier) =>
55+
node.type === 'Identifier' && node.name === 'require'
56+
57+
const isRequirePath = (node: StringLiteral, path: string) => node.type === 'StringLiteral' && node.value === path

src/imports.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Statement } from '@babel/types'
2+
3+
import { isImport, isRequire } from './helpers.js'
4+
import { nonNullable } from './utils.js'
5+
6+
// Finds import/require statements of a given path in an AST.
7+
export const getImports = (node: Statement, importPath: string) => {
8+
const esmImports = getImportsFromESM(node, importPath)
9+
10+
if (esmImports.length !== 0) {
11+
return esmImports
12+
}
13+
14+
const cjsImports = getImportsFromCJS(node, importPath)
15+
16+
return cjsImports
17+
}
18+
19+
// Finds import/require statements of a given path in a CJS AST.
20+
const getImportsFromCJS = (node: Statement, importPath: string) => {
21+
if (node.type !== 'VariableDeclaration') {
22+
return []
23+
}
24+
25+
const { declarations } = node
26+
const requireDeclaration = declarations.find(
27+
(declaration) => declaration.type === 'VariableDeclarator' && isRequire(declaration.init, importPath),
28+
)
29+
30+
if (requireDeclaration === undefined || requireDeclaration.id.type !== 'ObjectPattern') {
31+
return []
32+
}
33+
34+
const imports = requireDeclaration.id.properties
35+
.map((property) => {
36+
if (property.type !== 'ObjectProperty') {
37+
return
38+
}
39+
40+
const { key, value } = property
41+
42+
if (key.type !== 'Identifier' || value.type !== 'Identifier') {
43+
return
44+
}
45+
46+
return { imported: key.name, local: value.name }
47+
})
48+
.filter(nonNullable)
49+
50+
return imports
51+
}
52+
53+
// Finds import/require statements of a given path in an ESM AST.
54+
const getImportsFromESM = (node: Statement, importPath: string) => {
55+
if (!isImport(node, importPath)) {
56+
return []
57+
}
58+
59+
const { specifiers } = node
60+
61+
const imports = specifiers
62+
.map((specifier) => {
63+
if (specifier.type !== 'ImportSpecifier') {
64+
return
65+
}
66+
67+
const { imported, local } = specifier
68+
69+
if (imported.type !== 'Identifier' || local.type !== 'Identifier') {
70+
return
71+
}
72+
73+
return { imported: imported.name, local: local.name }
74+
})
75+
.filter(nonNullable)
76+
77+
return imports
78+
}

0 commit comments

Comments
 (0)