Skip to content

Commit 1c8968d

Browse files
netlify-team-account-1eduardoboucasLukas HolzerSkn0tt
authoredJan 28, 2022
feat: add local dev experience for scheduled functions (#3689)
* feat: add local dev experience for scheduled functions * feat: use synchronous function's timeout * chore: rename to scheduled.js * feat: return 400 when trying to invoke scheduled function via http * chore: use testMatrix pattern * fix: config-based schedule without esbuild * feat: add support for ISC-declared flags * fix: node v12 doesn't understand optional chaining * fix: allow esbuild to read mock files by writing them to disk * fix: wrong import * feat: use listFunction to detect ISC schedule * feat: remove unused feature flag * chore: update zisi * fix: enable parseISC hook * feat: give full command * refactor: move clockwork simulation to calling side * chore: remove one changed line * refactor: extract clockwork useragent into constants * feat: improve error message co-authored-by: Eduardo Boucas <eduardo@netlify.com> * feat: print friendly error screen * chore: trim down diff to npm-shrinkwrap * chore: remove mock-require (not used anymore) * fix: optional chaining doesnt exist * chore: add test for helpful tips and tricks * fix: correct tests * fix: add missing property to test * Update src/lib/functions/runtimes/js/builders/zisi.js Co-authored-by: Lukas Holzer <lukas.holzer@netlify.com> * refactor: extract help response into its own testable function * chore: add some unit tests * fix: replaceAll not available on node v12 co-authored-by: Lukas Holzer <lukas.holzer@netlify.com> * fix: remove unneeded level field * refactor: remove unused test matrix * fix: increase file change delay for macOS and windows Co-authored-by: Netlify Team Account 1 <netlify-team-account-1@users.noreply.github.com> Co-authored-by: Eduardo Boucas <eduardo@netlify.com> Co-authored-by: Lukas Holzer <lukas.holzer@netlify.com> Co-authored-by: Simon Knott <info@simonknott.de>
1 parent 40b1cf9 commit 1c8968d

15 files changed

+391
-104
lines changed
 

‎npm-shrinkwrap.json

+18-59
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@sindresorhus/slugify": "^1.1.0",
9090
"ansi-escapes": "^5.0.0",
9191
"ansi-styles": "^5.0.0",
92+
"ansi2html": "^0.0.1",
9293
"ascii-table": "0.0.9",
9394
"backoff": "^2.5.0",
9495
"better-opn": "^3.0.0",
@@ -103,6 +104,7 @@
103104
"content-type": "^1.0.4",
104105
"cookie": "^0.4.0",
105106
"copy-template-dir": "^1.4.0",
107+
"cron-parser": "^4.2.1",
106108
"debug": "^4.1.1",
107109
"decache": "^4.6.0",
108110
"del": "^6.0.0",
@@ -197,7 +199,6 @@
197199
"ini": "^2.0.0",
198200
"jsonwebtoken": "^8.5.1",
199201
"mock-fs": "^5.1.2",
200-
"mock-require": "^3.0.3",
201202
"p-timeout": "^4.0.0",
202203
"proxyquire": "^2.1.3",
203204
"seedrandom": "^3.0.5",

‎src/commands/functions/functions-invoke.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ const fs = require('fs')
33
const path = require('path')
44
const process = require('process')
55

6+
const CronParser = require('cron-parser')
67
const inquirer = require('inquirer')
78
const fetch = require('node-fetch')
89

9-
const { BACKGROUND, NETLIFYDEVWARN, chalk, error, exit, getFunctions } = require('../../utils')
10+
const { BACKGROUND, CLOCKWORK_USERAGENT, NETLIFYDEVWARN, chalk, error, exit, getFunctions } = require('../../utils')
1011

1112
// https://www.netlify.com/docs/functions/#event-triggered-functions
1213
const events = [
@@ -130,6 +131,13 @@ const getFunctionToTrigger = function (options, argumentName) {
130131
return argumentName
131132
}
132133

134+
const getNextRun = function (schedule) {
135+
const cron = CronParser.parseExpression(schedule, {
136+
tz: 'Etc/UTC',
137+
})
138+
return cron.next().toDate()
139+
}
140+
133141
/**
134142
* The functions:invoke command
135143
* @param {string} nameArgument
@@ -150,11 +158,18 @@ const functionsInvoke = async (nameArgument, options, command) => {
150158

151159
const functions = await getFunctions(functionsDir)
152160
const functionToTrigger = await getNameFromArgs(functions, options, nameArgument)
161+
const functionObj = functions.find((func) => func.name === functionToTrigger)
153162

154163
let headers = {}
155164
let body = {}
156165

157-
if (eventTriggeredFunctions.has(functionToTrigger)) {
166+
if (functionObj.schedule) {
167+
body.next_run = getNextRun(functionObj.schedule)
168+
headers = {
169+
'user-agent': CLOCKWORK_USERAGENT,
170+
'X-NF-Event': 'schedule',
171+
}
172+
} else if (eventTriggeredFunctions.has(functionToTrigger)) {
158173
/** handle event triggered fns */
159174
// https://www.netlify.com/docs/functions/#event-triggered-functions
160175
const [name, event] = functionToTrigger.split('-')

‎src/lib/functions/netlify-function.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class NetlifyFunction {
3232
// Determines whether this is a background function based on the function
3333
// name.
3434
this.isBackground = name.endsWith(BACKGROUND_SUFFIX)
35+
this.schedule = null
3536

3637
// List of the function's source files. This starts out as an empty set
3738
// and will get populated on every build.
@@ -44,6 +45,12 @@ class NetlifyFunction {
4445
return /^[A-Za-z0-9_-]+$/.test(this.name)
4546
}
4647

48+
async isScheduled() {
49+
await this.buildQueue
50+
51+
return Boolean(this.schedule)
52+
}
53+
4754
// The `build` method transforms source files into invocable functions. Its
4855
// return value is an object with:
4956
//
@@ -61,12 +68,13 @@ class NetlifyFunction {
6168
this.buildQueue = buildFunction({ cache })
6269

6370
try {
64-
const { srcFiles, ...buildData } = await this.buildQueue
71+
const { schedule, srcFiles, ...buildData } = await this.buildQueue
6572
const srcFilesSet = new Set(srcFiles)
6673
const srcFilesDiff = this.getSrcFilesDiff(srcFilesSet)
6774

6875
this.buildData = buildData
6976
this.srcFiles = srcFilesSet
77+
this.schedule = schedule
7078

7179
return { srcFilesDiff }
7280
} catch (error) {

‎src/lib/functions/runtimes/js/builders/zisi.js

+28-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { mkdir, writeFile } = require('fs').promises
22
const path = require('path')
33

4-
const { zipFunction } = require('@netlify/zip-it-and-ship-it')
4+
const { listFunction, zipFunction } = require('@netlify/zip-it-and-ship-it')
55
const decache = require('decache')
66
const readPkgUp = require('read-pkg-up')
77
const sourceMapSupport = require('source-map-support')
@@ -35,7 +35,11 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
3535
// root of the functions directory (e.g. `functions/my-func.js`). In
3636
// this case, we use `mainFile` as the function path of `zipFunction`.
3737
const entryPath = functionDirectory === directory ? func.mainFile : functionDirectory
38-
const { inputs, path: functionPath } = await memoizedBuild({
38+
const {
39+
inputs,
40+
path: functionPath,
41+
schedule,
42+
} = await memoizedBuild({
3943
cache,
4044
cacheKey: `zisi-${entryPath}`,
4145
command: () => zipFunction(entryPath, targetDirectory, zipOptions),
@@ -56,7 +60,22 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
5660

5761
clearFunctionsCache(targetDirectory)
5862

59-
return { buildPath, srcFiles }
63+
return { buildPath, srcFiles, schedule }
64+
}
65+
66+
/**
67+
* @param {object} params
68+
* @param {unknown} params.config
69+
* @param {string} params.mainFile
70+
* @param {string} params.projectRoot
71+
*/
72+
const parseForSchedule = async ({ config, mainFile, projectRoot }) => {
73+
const listedFunction = await listFunction(mainFile, {
74+
config: netlifyConfigToZisiConfig({ config, projectRoot }),
75+
parseISC: true,
76+
})
77+
78+
return listedFunction && listedFunction.schedule
6079
}
6180

6281
// Clears the cache for any files inside the directory from which functions are
@@ -79,10 +98,11 @@ const getTargetDirectory = async ({ errorExit }) => {
7998
return targetDirectory
8099
}
81100

101+
const netlifyConfigToZisiConfig = ({ config, projectRoot }) =>
102+
addFunctionsConfigDefaults(normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot }))
103+
82104
module.exports = async ({ config, directory, errorExit, func, projectRoot }) => {
83-
const functionsConfig = addFunctionsConfigDefaults(
84-
normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot }),
85-
)
105+
const functionsConfig = netlifyConfigToZisiConfig({ config, projectRoot })
86106

87107
const packageJson = await readPkgUp(func.mainFile)
88108
const hasTypeModule = packageJson && packageJson.packageJson.type === 'module'
@@ -115,3 +135,5 @@ module.exports = async ({ config, directory, errorExit, func, projectRoot }) =>
115135
target: targetDirectory,
116136
}
117137
}
138+
139+
module.exports.parseForSchedule = parseForSchedule

‎src/lib/functions/runtimes/js/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ const getBuildFunction = async ({ config, directory, errorExit, func, projectRoo
4646
// main file otherwise.
4747
const functionDirectory = dirname(func.mainFile)
4848
const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory]
49+
const schedule = await detectZisiBuilder.parseForSchedule({ mainFile: func.mainFile, config, projectRoot })
4950

50-
return () => ({ srcFiles })
51+
return () => ({ schedule, srcFiles })
5152
}
5253

5354
const invokeFunction = async ({ context, event, func, timeout }) => {

‎src/lib/functions/scheduled.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
const ansi2html = require('ansi2html')
2+
3+
const { CLOCKWORK_USERAGENT } = require('../../utils')
4+
5+
const { formatLambdaError } = require('./utils')
6+
7+
const buildHelpResponse = ({ error, headers, path, result }) => {
8+
const acceptsHtml = headers.accept && headers.accept.includes('text/html')
9+
10+
const paragraph = (text) => {
11+
text = text.trim()
12+
13+
if (acceptsHtml) {
14+
return ansi2html(`<p>${text}</p>`)
15+
}
16+
17+
text = text
18+
.replace(/<pre><code>/gm, '```\n')
19+
.replace(/<\/code><\/pre>/gm, '\n```')
20+
.replace(/<code>/gm, '`')
21+
.replace(/<\/code>/gm, '`')
22+
23+
return `${text}\n\n`
24+
}
25+
26+
const isSimulatedRequest = headers['user-agent'] === CLOCKWORK_USERAGENT
27+
28+
let message = ''
29+
30+
if (!isSimulatedRequest) {
31+
message += paragraph(`
32+
You performed an HTTP request to <code>${path}</code>, which is a scheduled function.
33+
You can do this to test your functions locally, but it won't work in production.
34+
`)
35+
}
36+
37+
if (error) {
38+
message += paragraph(`
39+
There was an error during execution of your scheduled function:
40+
41+
<pre><code>${formatLambdaError(error)}</code></pre>`)
42+
}
43+
44+
if (result) {
45+
// lambda emulator adds level field, which isn't user-provided
46+
const returnValue = { ...result }
47+
delete returnValue.level
48+
49+
const { statusCode } = returnValue
50+
if (statusCode >= 500) {
51+
message += paragraph(`
52+
Your function returned a status code of <code>${statusCode}</code>.
53+
At the moment, Netlify does nothing about that. In the future, there might be a retry mechanism based on this.
54+
`)
55+
}
56+
57+
const allowedKeys = new Set(['statusCode'])
58+
const returnedKeys = Object.keys(returnValue)
59+
const ignoredKeys = returnedKeys.filter((key) => !allowedKeys.has(key))
60+
61+
if (ignoredKeys.length !== 0) {
62+
message += paragraph(
63+
`Your function returned ${ignoredKeys
64+
.map((key) => `<code>${key}</code>`)
65+
.join(', ')}. Is this an accident? It won't be interpreted by Netlify.`,
66+
)
67+
}
68+
}
69+
70+
const statusCode = error ? 500 : 200
71+
return acceptsHtml
72+
? {
73+
statusCode,
74+
contentType: 'text/html',
75+
message: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">\n
76+
${message}`,
77+
}
78+
: {
79+
statusCode,
80+
contentType: 'text/plain',
81+
message,
82+
}
83+
}
84+
85+
const handleScheduledFunction = ({ error, request, response, result }) => {
86+
const { contentType, message, statusCode } = buildHelpResponse({
87+
error,
88+
headers: request.headers,
89+
path: request.path,
90+
result,
91+
})
92+
93+
response.status(statusCode)
94+
response.set('Content-Type', contentType)
95+
response.send(message)
96+
}
97+
98+
module.exports = { handleScheduledFunction, buildHelpResponse }

‎src/lib/functions/scheduled.test.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const test = require('ava')
2+
3+
const { buildHelpResponse } = require('./scheduled')
4+
5+
const withAccept = (accept) =>
6+
buildHelpResponse({
7+
error: undefined,
8+
headers: {
9+
accept,
10+
},
11+
path: '/',
12+
result: {
13+
statusCode: 200,
14+
},
15+
})
16+
17+
test('buildHelpResponse does content negotiation', (t) => {
18+
const html = withAccept('text/html')
19+
t.is(html.contentType, 'text/html')
20+
t.true(html.message.includes('<link rel='))
21+
t.true(html.message.includes('<p>'))
22+
23+
const plain = withAccept('text/plain')
24+
t.is(plain.contentType, 'text/plain')
25+
t.false(plain.message.includes('<link rel='))
26+
t.false(plain.message.includes('<p>'))
27+
})
28+
29+
test('buildHelpResponse prints errors', (t) => {
30+
const response = buildHelpResponse({
31+
error: new Error('test'),
32+
headers: {},
33+
path: '/',
34+
result: {
35+
statusCode: 200,
36+
},
37+
})
38+
39+
t.true(response.message.includes('There was an error'))
40+
})
41+
42+
const withUserAgent = (userAgent) =>
43+
buildHelpResponse({
44+
error: new Error('test'),
45+
headers: {
46+
accept: 'text/plain',
47+
'user-agent': userAgent,
48+
},
49+
path: '/',
50+
result: {
51+
statusCode: 200,
52+
},
53+
})
54+
55+
test('buildHelpResponse conditionally prints notice about HTTP x scheduled functions', (t) => {
56+
t.true(withUserAgent('').message.includes("it won't work in production"))
57+
t.false(withUserAgent('Netlify Clockwork').message.includes("it won't work in production"))
58+
})

‎src/lib/functions/server.js

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { NETLIFYDEVERR, NETLIFYDEVLOG, error: errorExit, getInternalFunctionsDir,
66
const { handleBackgroundFunction, handleBackgroundFunctionResult } = require('./background')
77
const { createFormSubmissionHandler } = require('./form-submissions-handler')
88
const { FunctionsRegistry } = require('./registry')
9+
const { handleScheduledFunction } = require('./scheduled')
910
const { handleSynchronousFunction } = require('./synchronous')
1011
const { shouldBase64Encode } = require('./utils')
1112

@@ -105,6 +106,15 @@ const createHandler = function ({ functionsRegistry }) {
105106
const { error } = await func.invoke(event, clientContext)
106107

107108
handleBackgroundFunctionResult(functionName, error)
109+
} else if (await func.isScheduled()) {
110+
const { error, result } = await func.invoke(event, clientContext)
111+
112+
handleScheduledFunction({
113+
error,
114+
result,
115+
request,
116+
response,
117+
})
108118
} else {
109119
const { error, result } = await func.invoke(event, clientContext)
110120

‎src/lib/functions/server.test.js

+10-31
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,37 @@
1-
const fs = require('fs')
2-
const { platform } = require('os')
1+
const { mkdirSync, mkdtempSync, writeFileSync } = require('fs')
2+
const { tmpdir } = require('os')
33
const { join } = require('path')
44

5-
const zisi = require('@netlify/zip-it-and-ship-it')
65
const test = require('ava')
76
const express = require('express')
8-
const mockRequire = require('mock-require')
9-
const sinon = require('sinon')
107
const request = require('supertest')
118

12-
const projectRoot = platform() === 'win32' ? 'C:\\my-functions' : `/my-functions`
13-
const functionsPath = `functions`
14-
15-
// mock mkdir for the functions folder
16-
sinon.stub(fs.promises, 'mkdir').withArgs(join(projectRoot, functionsPath)).returns(Promise.resolve())
17-
189
const { FunctionsRegistry } = require('./registry')
1910
const { createHandler } = require('./server')
2011

2112
/** @type { express.Express} */
2213
let app
2314

24-
test.before(async (t) => {
25-
const mainFile = join(projectRoot, functionsPath, 'hello.js')
26-
t.context.zisiStub = sinon.stub(zisi, 'listFunctions').returns(
27-
Promise.resolve([
28-
{
29-
name: 'hello',
30-
mainFile,
31-
runtime: 'js',
32-
extension: '.js',
33-
},
34-
]),
35-
)
15+
test.before(async () => {
16+
const projectRoot = mkdtempSync(join(tmpdir(), 'functions-server-project-root'))
17+
const functionsDirectory = join(projectRoot, 'functions')
18+
mkdirSync(functionsDirectory)
19+
20+
const mainFile = join(functionsDirectory, 'hello.js')
21+
writeFileSync(mainFile, `exports.handler = (event) => ({ statusCode: 200, body: event.rawUrl })`)
3622

37-
mockRequire(mainFile, {
38-
handler: (event) => ({ statusCode: 200, body: event.rawUrl }),
39-
})
4023
const functionsRegistry = new FunctionsRegistry({
4124
projectRoot,
4225
config: {},
4326
timeouts: { syncFunctions: 1, backgroundFunctions: 1 },
4427
// eslint-disable-next-line no-magic-numbers
4528
settings: { port: 8888 },
4629
})
47-
await functionsRegistry.scan([functionsPath])
30+
await functionsRegistry.scan([functionsDirectory])
4831
app = express()
4932
app.all('*', createHandler({ functionsRegistry }))
5033
})
5134

52-
test.after((t) => {
53-
t.context.zisiStub.restore()
54-
})
55-
5635
test('should get the url as the `rawUrl` inside the function', async (t) => {
5736
await request(app)
5837
.get('/hello')

‎src/utils/functions/constants.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const CLOCKWORK_USERAGENT = 'Netlify Clockwork'
2+
3+
module.exports = {
4+
CLOCKWORK_USERAGENT,
5+
}

‎src/utils/functions/get-functions.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ const getUrlPath = (functionName) => `/.netlify/functions/${functionName}`
55

66
const BACKGROUND = '-background'
77

8-
const addFunctionProps = ({ mainFile, name, runtime }) => {
8+
const addFunctionProps = ({ mainFile, name, runtime, schedule }) => {
99
const urlPath = getUrlPath(name)
1010
const isBackground = name.endsWith(BACKGROUND)
11-
return { mainFile, name, runtime, urlPath, isBackground }
11+
return { mainFile, name, runtime, urlPath, isBackground, schedule }
1212
}
1313

1414
const JS = 'js'
@@ -21,7 +21,9 @@ const getFunctions = async (functionsSrcDir) => {
2121
// performance optimization, load '@netlify/zip-it-and-ship-it' on demand
2222
// eslint-disable-next-line node/global-require
2323
const { listFunctions } = require('@netlify/zip-it-and-ship-it')
24-
const functions = await listFunctions(functionsSrcDir)
24+
const functions = await listFunctions(functionsSrcDir, {
25+
parseISC: true,
26+
})
2527
const functionsWithProps = functions.filter(({ runtime }) => runtime === JS).map((func) => addFunctionProps(func))
2628
return functionsWithProps
2729
}

‎src/utils/functions/get-functions.test.js

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ test('should return object with function details for a directory with js files',
3737
mainFile: path.join(builder.directory, 'functions', 'index.js'),
3838
isBackground: false,
3939
runtime: 'js',
40+
schedule: undefined,
4041
urlPath: '/.netlify/functions/index',
4142
},
4243
])
@@ -64,13 +65,15 @@ test('should mark background functions based on filenames', async (t) => {
6465
mainFile: path.join(builder.directory, 'functions', 'bar-background', 'bar-background.js'),
6566
isBackground: true,
6667
runtime: 'js',
68+
schedule: undefined,
6769
urlPath: '/.netlify/functions/bar-background',
6870
},
6971
{
7072
name: 'foo-background',
7173
mainFile: path.join(builder.directory, 'functions', 'foo-background.js'),
7274
isBackground: true,
7375
runtime: 'js',
76+
schedule: undefined,
7477
urlPath: '/.netlify/functions/foo-background',
7578
},
7679
])

‎src/utils/functions/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
const constants = require('./constants')
12
const edgeHandlers = require('./edge-handlers')
23
const functions = require('./functions')
34
const getFunctions = require('./get-functions')
45

56
module.exports = {
7+
...constants,
68
...functions,
79
...edgeHandlers,
810
...getFunctions,

‎tests/command.functions.test.js

+124
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { withDevServer } = require('./utils/dev-server')
1313
const got = require('./utils/got')
1414
const { CONFIRM, DOWN, answerWithValue, handleQuestions } = require('./utils/handle-questions')
1515
const { withMockApi } = require('./utils/mock-api')
16+
const { pause } = require('./utils/pause')
1617
const { killProcess } = require('./utils/process')
1718
const { withSiteBuilder } = require('./utils/site-builder')
1819

@@ -582,6 +583,129 @@ test('should trigger background function from event', async (t) => {
582583
})
583584
})
584585

586+
test('should serve helpful tips and tricks', async (t) => {
587+
await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
588+
await builder
589+
.withNetlifyToml({
590+
config: { functions: { directory: 'functions' } },
591+
})
592+
// mocking until https://github.com/netlify/functions/pull/226 landed
593+
.withContentFile({
594+
path: 'node_modules/@netlify/functions/package.json',
595+
content: `{}`,
596+
})
597+
.withContentFile({
598+
path: 'node_modules/@netlify/functions/index.js',
599+
content: `
600+
module.exports.schedule = (schedule, handler) => handler
601+
`,
602+
})
603+
.withContentFile({
604+
path: 'functions/hello-world.js',
605+
content: `
606+
const { schedule } = require('@netlify/functions')
607+
608+
module.exports.handler = schedule('@daily', () => {
609+
return {
610+
statusCode: 200,
611+
body: "hello world"
612+
}
613+
})
614+
`.trim(),
615+
})
616+
.buildAsync()
617+
618+
await withDevServer({ cwd: builder.directory }, async (server) => {
619+
const plainTextResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
620+
throwHttpErrors: false,
621+
retry: null,
622+
})
623+
const youReturnedBodyRegex = /.*Your function returned `body`. Is this an accident\?.*/
624+
t.regex(plainTextResponse.body, youReturnedBodyRegex)
625+
t.regex(plainTextResponse.body, /.*You performed an HTTP request.*/)
626+
t.is(plainTextResponse.statusCode, 200)
627+
628+
const htmlResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
629+
throwHttpErrors: false,
630+
retry: null,
631+
headers: {
632+
accept: 'text/html',
633+
},
634+
})
635+
t.regex(htmlResponse.body, /.*<link.*/)
636+
t.is(htmlResponse.statusCode, 200)
637+
638+
const stdout = await callCli(['functions:invoke', 'hello-world', '--identity', `--port=${server.port}`], {
639+
cwd: builder.directory,
640+
})
641+
t.regex(stdout, youReturnedBodyRegex)
642+
})
643+
})
644+
})
645+
646+
test('should detect file changes to scheduled function', async (t) => {
647+
await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
648+
await builder
649+
.withNetlifyToml({
650+
config: { functions: { directory: 'functions' } },
651+
})
652+
// mocking until https://github.com/netlify/functions/pull/226 landed
653+
.withContentFile({
654+
path: 'node_modules/@netlify/functions/package.json',
655+
content: `{}`,
656+
})
657+
.withContentFile({
658+
path: 'node_modules/@netlify/functions/index.js',
659+
content: `
660+
module.exports.schedule = (schedule, handler) => handler
661+
`,
662+
})
663+
.withContentFile({
664+
path: 'functions/hello-world.js',
665+
content: `
666+
module.exports.handler = () => {
667+
return {
668+
statusCode: 200
669+
}
670+
}
671+
`.trim(),
672+
})
673+
.buildAsync()
674+
675+
await withDevServer({ cwd: builder.directory }, async (server) => {
676+
const helloWorldBody = () =>
677+
got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
678+
throwHttpErrors: false,
679+
retry: null,
680+
}).then((response) => response.body)
681+
682+
t.is(await helloWorldBody(), '')
683+
684+
await builder
685+
.withContentFile({
686+
path: 'functions/hello-world.js',
687+
content: `
688+
const { schedule } = require('@netlify/functions')
689+
690+
module.exports.handler = schedule("@daily", () => {
691+
return {
692+
statusCode: 200,
693+
body: "test"
694+
}
695+
})
696+
`.trim(),
697+
})
698+
.buildAsync()
699+
700+
const DETECT_FILE_CHANGE_DELAY = 500
701+
await pause(DETECT_FILE_CHANGE_DELAY)
702+
703+
const warningMessage = await helloWorldBody()
704+
t.true(warningMessage.includes('Your function returned `body`'))
705+
})
706+
})
707+
})
708+
585709
test('should inject env variables', async (t) => {
586710
await withSiteBuilder('site-with-env-function', async (builder) => {
587711
await builder

1 commit comments

Comments
 (1)

github-actions[bot] commented on Jan 28, 2022

@github-actions[bot]

📊 Benchmark results

Package size: 360 MB

Please sign in to comment.