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

Commit aaa06bf

Browse files
committed
feat: bring in new changes from ZISI
1 parent 0f23188 commit aaa06bf

13 files changed

+249
-20
lines changed

src/bindings.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Expression, Statement, VariableDeclaration } from '@babel/types'
2+
3+
type Bindings = Map<string, Expression>
4+
5+
const getBindingFromVariableDeclaration = function (node: VariableDeclaration, bindings: Bindings): void {
6+
node.declarations.forEach((declaration) => {
7+
if (declaration.id.type === 'Identifier' && declaration.init) {
8+
bindings.set(declaration.id.name, declaration.init)
9+
}
10+
})
11+
}
12+
13+
// eslint-disable-next-line complexity
14+
const getBindingsFromNode = function (node: Statement, bindings: Bindings): void {
15+
if (node.type === 'VariableDeclaration') {
16+
// A variable was created, so create it and store the potential value
17+
getBindingFromVariableDeclaration(node, bindings)
18+
} else if (
19+
node.type === 'ExpressionStatement' &&
20+
node.expression.type === 'AssignmentExpression' &&
21+
node.expression.left.type === 'Identifier'
22+
) {
23+
// The variable was reassigned, so let's store the new value
24+
bindings.set(node.expression.left.name, node.expression.right)
25+
} else if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {
26+
// A `export const|let ...` creates a binding that can later be referenced again
27+
getBindingFromVariableDeclaration(node.declaration, bindings)
28+
}
29+
}
30+
31+
/**
32+
* Goes through all relevant nodes and creates a map from binding name to assigned value/expression
33+
*/
34+
const getAllBindings = function (nodes: Statement[]): Bindings {
35+
const bindings: Bindings = new Map()
36+
37+
nodes.forEach((node) => {
38+
getBindingsFromNode(node, bindings)
39+
})
40+
41+
return bindings
42+
}
43+
44+
export type BindingMethod = () => Bindings
45+
46+
export const createBindingsMethod = function (nodes: Statement[]): BindingMethod {
47+
// memoize the result for these nodes
48+
let result: Bindings
49+
50+
return () => {
51+
if (!result) {
52+
result = getAllBindings(nodes)
53+
}
54+
55+
return result
56+
}
57+
}

src/exports.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { CallExpression, Statement } from '@babel/types'
1+
import type { ExportNamedDeclaration, ExportSpecifier, Expression, Statement } from '@babel/types'
22

3+
import type { BindingMethod } from './bindings.js'
34
import { isModuleExports } from './helpers.js'
45

56
import type { ISCExport } from './index.js'
67

78
// Finds the main handler export in an AST.
8-
export const getMainExport = (nodes: Statement[]) => {
9+
export const getMainExport = (nodes: Statement[], getAllBindings: BindingMethod) => {
910
let handlerExport: ISCExport[] = []
1011

1112
nodes.find((node) => {
12-
const esmExports = getMainExportFromESM(node)
13+
const esmExports = getMainExportFromESM(node, getAllBindings)
1314

1415
if (esmExports.length !== 0) {
1516
handlerExport = esmExports
@@ -39,24 +40,27 @@ const getMainExportFromCJS = (node: Statement) => {
3940
]
4041

4142
return handlerPaths.flatMap((handlerPath) => {
42-
if (!isModuleExports(node, handlerPath) || node.expression.right.type !== 'CallExpression') {
43+
if (!isModuleExports(node, handlerPath)) {
4344
return []
4445
}
4546

46-
return getExportsFromCallExpression(node.expression.right)
47+
return getExportsFromExpression(node.expression.right)
4748
})
4849
}
4950

5051
// Finds the main handler export in an ESM AST.
51-
// eslint-disable-next-line complexity
52-
const getMainExportFromESM = (node: Statement) => {
52+
const getMainExportFromESM = (node: Statement, getAllBindings: BindingMethod) => {
5353
if (node.type !== 'ExportNamedDeclaration' || node.exportKind !== 'value') {
5454
return []
5555
}
5656

57-
const { declaration } = node
57+
const { declaration, specifiers } = node
5858

59-
if (!declaration || declaration.type !== 'VariableDeclaration') {
59+
if (specifiers?.length > 0) {
60+
return getExportsFromBindings(specifiers, getAllBindings)
61+
}
62+
63+
if (declaration?.type !== 'VariableDeclaration') {
6064
return []
6165
}
6266

@@ -66,16 +70,47 @@ const getMainExportFromESM = (node: Statement) => {
6670
return type === 'VariableDeclarator' && id.type === 'Identifier' && id.name === 'handler'
6771
})
6872

69-
if (handlerDeclaration?.init?.type !== 'CallExpression') {
73+
const exports = getExportsFromExpression(handlerDeclaration?.init)
74+
75+
return exports
76+
}
77+
78+
// Check if the Node is an ExportSpecifier that has a named export called `handler`
79+
// either with Identifier `export { handler }`
80+
// or with StringLiteral `export { x as "handler" }`
81+
const isHandlerExport = (node: ExportNamedDeclaration['specifiers'][number]): node is ExportSpecifier => {
82+
const { type, exported } = node
83+
84+
return (
85+
type === 'ExportSpecifier' &&
86+
((exported.type === 'Identifier' && exported.name === 'handler') ||
87+
(exported.type === 'StringLiteral' && exported.value === 'handler'))
88+
)
89+
}
90+
91+
// Tries to resolve the export from a binding (variable)
92+
// for example `let handler; handler = () => {}; export { handler }` would
93+
// resolve correctly to the handler function
94+
const getExportsFromBindings = (specifiers: ExportNamedDeclaration['specifiers'], getAllBindings: BindingMethod) => {
95+
const specifier = specifiers.find(isHandlerExport)
96+
97+
if (!specifier) {
7098
return []
7199
}
72100

73-
const exports = getExportsFromCallExpression(handlerDeclaration.init)
101+
const binding = getAllBindings().get(specifier.local.name)
102+
const exports = getExportsFromExpression(binding)
74103

75104
return exports
76105
}
77106

78-
const getExportsFromCallExpression = (node: CallExpression) => {
107+
const getExportsFromExpression = (node: Expression | undefined | null) => {
108+
// We're only interested in expressions representing function calls, because
109+
// the ISC patterns we implement at the moment are all helper functions.
110+
if (node?.type !== 'CallExpression') {
111+
return []
112+
}
113+
79114
const { arguments: args, callee } = node
80115

81116
if (callee.type !== 'Identifier') {

src/index.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs'
33
import { parse } from '@babel/parser'
44
import { ArgumentPlaceholder, Expression, SpreadElement, JSXNamespacedName, Program } from '@babel/types'
55

6+
import { createBindingsMethod } from './bindings.js'
67
import { getMainExport } from './exports.js'
78
import { getImports } from './imports.js'
89
import { parse as parseSchedule } from './properties/schedule.js'
@@ -22,7 +23,12 @@ interface ISCConfig {
2223
// the property and `data` its value.
2324
export const findISCDeclarationsInProgram = (ast: Program, config: ISCConfig): ISCValues => {
2425
const imports = ast.body.flatMap((node) => getImports(node, config.isHelperModule))
25-
const mainExports = getMainExport(ast.body)
26+
27+
const scheduledFuncsExpected = imports.filter(({ imported }) => imported === 'schedule').length
28+
let scheduledFuncsFound = 0
29+
30+
const getAllBindings = createBindingsMethod(ast.body)
31+
const mainExports = getMainExport(ast.body, getAllBindings)
2632
const iscExports = mainExports
2733
.map(({ args, local: exportName }) => {
2834
const matchingImport = imports.find(({ local: importName }) => importName === exportName)
@@ -32,16 +38,29 @@ export const findISCDeclarationsInProgram = (ast: Program, config: ISCConfig): I
3238
}
3339

3440
switch (matchingImport.imported) {
35-
case 'schedule':
36-
return parseSchedule({ args })
41+
case 'schedule': {
42+
const parsed = parseSchedule({ args }, getAllBindings)
43+
44+
if (parsed.schedule) {
45+
scheduledFuncsFound += 1
46+
}
3747

48+
return parsed
49+
}
3850
default:
3951
// no-op
4052
}
4153

4254
return null
4355
})
4456
.filter(nonNullable)
57+
58+
if (scheduledFuncsFound < scheduledFuncsExpected) {
59+
throw new Error(
60+
'Warning: unable to find cron expression for scheduled function. `schedule` imported but not called or exported. If you meant to schedule a function, please check that `schedule` is invoked with an appropriate cron expression.',
61+
)
62+
}
63+
4564
const mergedExports: ISCValues = iscExports.reduce((acc, obj) => ({ ...acc, ...obj }), {})
4665

4766
return mergedExports

src/properties/schedule.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import type { BindingMethod } from '../bindings.js'
12
import type { ISCHandlerArg } from '../index.js'
23

3-
export const parse = ({ args }: { args: ISCHandlerArg[] }) => {
4-
const [expression] = args
4+
export const parse = ({ args }: { args: ISCHandlerArg[] }, getAllBindings: BindingMethod) => {
5+
let [expression] = args
6+
7+
if (expression.type === 'Identifier') {
8+
const binding = getAllBindings().get(expression.name)
9+
10+
if (binding) {
11+
expression = binding
12+
}
13+
}
14+
515
const schedule = expression.type === 'StringLiteral' ? expression.value : undefined
616

717
return {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { schedule } = require('@netlify/functions')
2+
3+
module.exports.handler = schedule(null, () => {
4+
// function handler
5+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const { schedule } = require('@netlify/functions')
2+
3+
// Should throw an error that `schedule` is imported but cron expression not found
4+
module.exports.handler = {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { schedule as nfySchedule } from '@netlify/functions'
2+
import { schedule } from '../node_modules/@netlify/functions/index.js'
3+
// make sure cron expression is found/doesn't error if `schedule` is also imported from another source
4+
5+
schedule()
6+
7+
export const handler = nfySchedule('@daily', () => {
8+
// function handler
9+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { schedule } from '@netlify/functions'
2+
3+
const handler = schedule('@daily', async () => {
4+
// function handler
5+
})
6+
7+
export { handler }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { schedule } from '@netlify/functions'
2+
3+
const _handler = schedule('@daily', async () => {
4+
// function handler
5+
})
6+
export { _handler as handler }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { schedule } from '@netlify/functions'
2+
3+
const handler = async () => {
4+
// function handler
5+
}
6+
7+
const _handler = schedule('@daily', handler)
8+
export { _handler as handler }

0 commit comments

Comments
 (0)