Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

fix: annotate ISC errors and split into to separate errors #1146

Merged
merged 6 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const augmentWithISC = async (func: FunctionSource): Promise<AugmentedFunctionSo
return func
}

const inSourceConfig = await findISCDeclarationsInPath(func.mainFile)
const inSourceConfig = await findISCDeclarationsInPath(func.mainFile, func.name)

return { ...func, inSourceConfig }
}
Expand Down
33 changes: 25 additions & 8 deletions src/runtimes/node/in_source_config/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ArgumentPlaceholder, Expression, SpreadElement, JSXNamespacedName } from '@babel/types'

import { FunctionBundlingUserError } from '../../../utils/error.js'
import { nonNullable } from '../../../utils/non_nullable.js'
import { createBindingsMethod } from '../parser/bindings.js'
import { getMainExport } from '../parser/exports.js'
Expand All @@ -12,10 +13,26 @@ export const IN_SOURCE_CONFIG_MODULE = '@netlify/functions'

export type ISCValues = Partial<ReturnType<typeof parseSchedule>>

const validateScheduleFunction = (functionFound: boolean, scheduleFound: boolean, functionName: string): void => {
if (!functionFound) {
throw new FunctionBundlingUserError(
"The `schedule` helper was imported but we couldn't find any usages. If you meant to schedule a function, please check that `schedule` is invoked and `handler` correctly exported.",
{ functionName, runtime: 'js' },
)
}

if (!scheduleFound) {
throw new FunctionBundlingUserError(
'Unable to find cron expression for scheduled function. The cron expression (first argument) for the `schedule` helper needs to be accessible inside the file and cannot be imported.',
{ functionName, runtime: 'js' },
)
}
}

// Parses a JS/TS file and looks for in-source config declarations. It returns
// an array of all declarations found, with `property` indicating the name of
// the property and `data` its value.
export const findISCDeclarationsInPath = async (sourcePath: string): Promise<ISCValues> => {
export const findISCDeclarationsInPath = async (sourcePath: string, functionName: string): Promise<ISCValues> => {
const ast = await safelyParseFile(sourcePath)

if (ast === null) {
Expand All @@ -24,8 +41,9 @@ export const findISCDeclarationsInPath = async (sourcePath: string): Promise<ISC

const imports = ast.body.flatMap((node) => getImports(node, IN_SOURCE_CONFIG_MODULE))

const scheduledFuncsExpected = imports.filter(({ imported }) => imported === 'schedule').length
let scheduledFuncsFound = 0
const scheduledFunctionExpected = imports.some(({ imported }) => imported === 'schedule')
let scheduledFunctionFound = false
let scheduleFound = false
Copy link
Contributor Author

@danez danez Jul 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the logic a little because all we care if schedule was imported once, if someone imports multiple times, then that is fine too as long as one of the imports is used and exported.πŸ€·β€β™‚οΈ


const getAllBindings = createBindingsMethod(ast.body)
const mainExports = getMainExport(ast.body, getAllBindings)
Expand All @@ -41,8 +59,9 @@ export const findISCDeclarationsInPath = async (sourcePath: string): Promise<ISC
case 'schedule': {
const parsed = parseSchedule({ args }, getAllBindings)

scheduledFunctionFound = true
if (parsed.schedule) {
scheduledFuncsFound += 1
scheduleFound = true
}

return parsed
Expand All @@ -55,10 +74,8 @@ export const findISCDeclarationsInPath = async (sourcePath: string): Promise<ISC
})
.filter(nonNullable)

if (scheduledFuncsFound < scheduledFuncsExpected) {
throw new Error(
'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.',
)
if (scheduledFunctionExpected) {
validateScheduleFunction(scheduledFunctionFound, scheduleFound, functionName)
}

const mergedExports: ISCValues = iscExports.reduce((acc, obj) => ({ ...acc, ...obj }), {})
Expand Down
2 changes: 1 addition & 1 deletion src/runtimes/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const zipFunction: ZipFunction = async function ({
stat,
})

const inSourceConfig = await findISCDeclarationsInPath(mainFile)
const inSourceConfig = await findISCDeclarationsInPath(mainFile, name)

createPluginsModulesPathAliases(srcFiles, pluginsModulesPath, aliases, finalBasePath)

Expand Down
31 changes: 31 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NodeBundlerName } from '../runtimes/node/bundlers'
import { RuntimeName } from '../runtimes/runtime'

interface CustomErrorInfo {
type: 'functionsBundling'
location: {
functionName: string
runtime: RuntimeName
bundler?: NodeBundlerName
}
}

export class FunctionBundlingUserError extends Error {
danez marked this conversation as resolved.
Show resolved Hide resolved
customErrorInfo: CustomErrorInfo

constructor(
message: string,
customErrorInfo: {
functionName: string
runtime: RuntimeName
bundler?: NodeBundlerName
},
) {
super(message)

Object.setPrototypeOf(this, new.target.prototype)
this.name = 'FunctionBundlingUserError'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines are necessary because typescript does not support extending builtins completely
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget


this.customErrorInfo = { type: 'functionsBundling', location: customErrorInfo }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// eslint-disable-next-line n/no-unsupported-features/es-syntax
import { schedule } from '@netlify/functions'
import { schedule as schedule2 } from '@netlify/functions'

export const handler = schedule2('@daily', () => {})
34 changes: 29 additions & 5 deletions tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2633,7 +2633,7 @@ testMany(
'Finds in-source config declarations using the `schedule` helper',
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
async (options, t) => {
const FUNCTIONS_COUNT = 12
const FUNCTIONS_COUNT = 13
const { files } = await zipFixture(t, join('in-source-config', 'functions'), {
opts: options,
length: FUNCTIONS_COUNT,
Expand All @@ -2648,21 +2648,45 @@ testMany(
)

testMany(
'Throws error when `schedule` helper is imported but cron expression not found',
'Throws error when `schedule` helper is used but cron expression not found',
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
async (options, t) => {
const rejected = (error) => {
t.true(error.message.startsWith('Warning: unable to find cron expression for scheduled function.'))
t.true(error.message.startsWith('Unable to find cron expression for scheduled function.'))
t.is(error.customErrorInfo.type, 'functionsBundling')
t.is(error.customErrorInfo.location.bundler, undefined)
t.is(typeof error.customErrorInfo.location.functionName, 'string')
t.is(error.customErrorInfo.location.runtime, 'js')
}

const FUNCTIONS_COUNT = 3
const FUNCTIONS_COUNT = 1
await zipFixture(t, join('in-source-config', 'functions_missing_cron_expression'), {
opts: options,
length: FUNCTIONS_COUNT,
}).catch(rejected)
},
)

testMany(
'Throws error when `schedule` helper is imported but not used',
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
async (options, t) => {
const rejected = (error) => {
t.true(error.message.startsWith("The `schedule` helper was imported but we couldn't find any usages."))
t.is(error.customErrorInfo.type, 'functionsBundling')
t.is(error.customErrorInfo.location.bundler, undefined)
t.is(typeof error.customErrorInfo.location.functionName, 'string')
t.is(error.customErrorInfo.location.runtime, 'js')
}

const FUNCTIONS_COUNT = 2
await zipFixture(t, join('in-source-config', 'functions_missing_schedule_usage'), {
opts: options,
length: FUNCTIONS_COUNT,
}).catch(rejected)
},
)

test('listFunctions surfaces schedule config property', async (t) => {
const functions = await listFunctions(join(FIXTURES_DIR, 'many-functions'), {
config: {
Expand All @@ -2679,7 +2703,7 @@ test('listFunctions includes in-source config declarations', async (t) => {
const functions = await listFunctions(join(FIXTURES_DIR, 'in-source-config', 'functions'), {
parseISC: true,
})
const FUNCTIONS_COUNT = 12
const FUNCTIONS_COUNT = 13
t.is(functions.length, FUNCTIONS_COUNT)
functions.forEach((func) => {
t.is(func.schedule, '@daily')
Expand Down