netlify-cli 8.13.2 → 8.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "8.13.2",
3
+ "version": "8.15.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "netlify-cli",
9
- "version": "8.13.2",
9
+ "version": "8.15.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -22,6 +22,7 @@
22
22
  "@sindresorhus/slugify": "^1.1.0",
23
23
  "ansi-escapes": "^5.0.0",
24
24
  "ansi-styles": "^5.0.0",
25
+ "ansi2html": "^0.0.1",
25
26
  "ascii-table": "0.0.9",
26
27
  "backoff": "^2.5.0",
27
28
  "better-opn": "^3.0.0",
@@ -36,6 +37,7 @@
36
37
  "content-type": "^1.0.4",
37
38
  "cookie": "^0.4.0",
38
39
  "copy-template-dir": "^1.4.0",
40
+ "cron-parser": "^4.2.1",
39
41
  "debug": "^4.1.1",
40
42
  "decache": "^4.6.0",
41
43
  "del": "^6.0.0",
@@ -79,7 +81,7 @@
79
81
  "multiparty": "^4.2.1",
80
82
  "netlify": "^10.1.2",
81
83
  "netlify-headers-parser": "^6.0.1",
82
- "netlify-onegraph-internal": "0.0.15",
84
+ "netlify-onegraph-internal": "0.0.16",
83
85
  "netlify-redirect-parser": "^13.0.1",
84
86
  "netlify-redirector": "^0.2.1",
85
87
  "node-fetch": "^2.6.0",
@@ -122,7 +124,7 @@
122
124
  },
123
125
  "devDependencies": {
124
126
  "@babel/preset-react": "^7.12.13",
125
- "@netlify/eslint-config-node": "^4.1.6",
127
+ "@netlify/eslint-config-node": "^4.1.7",
126
128
  "ava": "^4.0.0",
127
129
  "c8": "^7.11.0",
128
130
  "eslint-plugin-sort-destructure-keys": "^1.3.5",
@@ -134,7 +136,6 @@
134
136
  "ini": "^2.0.0",
135
137
  "jsonwebtoken": "^8.5.1",
136
138
  "mock-fs": "^5.1.2",
137
- "mock-require": "^3.0.3",
138
139
  "p-timeout": "^4.0.0",
139
140
  "proxyquire": "^2.1.3",
140
141
  "seedrandom": "^3.0.5",
@@ -2566,9 +2567,9 @@
2566
2567
  }
2567
2568
  },
2568
2569
  "node_modules/@netlify/eslint-config-node": {
2569
- "version": "4.1.6",
2570
- "resolved": "https://registry.npmjs.org/@netlify/eslint-config-node/-/eslint-config-node-4.1.6.tgz",
2571
- "integrity": "sha512-G/6bqpeR8jQj1cbEjFg8sdc23Wq4TUz/fD+XFup0EYEJbUnjriClsljgMUSnmnBNllyMy36T9YEXkam4HUKtJA==",
2570
+ "version": "4.1.7",
2571
+ "resolved": "https://registry.npmjs.org/@netlify/eslint-config-node/-/eslint-config-node-4.1.7.tgz",
2572
+ "integrity": "sha512-3XYVE0Wnr8YmSTls7447fT4Lyr5goFJ1kK/xevdLVdlzinxd1Wc6SK4um5hZAvl6nrURmgRfLBSIqdpDeRFIlg==",
2572
2573
  "dev": true,
2573
2574
  "dependencies": {
2574
2575
  "@babel/core": "^7.13.8",
@@ -4426,6 +4427,17 @@
4426
4427
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
4427
4428
  }
4428
4429
  },
4430
+ "node_modules/ansi2html": {
4431
+ "version": "0.0.1",
4432
+ "resolved": "https://registry.npmjs.org/ansi2html/-/ansi2html-0.0.1.tgz",
4433
+ "integrity": "sha1-u4gARhtECvALkb89c2al4LhHO6g=",
4434
+ "bin": {
4435
+ "ansi2html": "bin/ansi2html"
4436
+ },
4437
+ "engines": {
4438
+ "node": ">0.4"
4439
+ }
4440
+ },
4429
4441
  "node_modules/any-observable": {
4430
4442
  "version": "0.3.0",
4431
4443
  "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
@@ -10887,12 +10899,6 @@
10887
10899
  "node": ">=6.0"
10888
10900
  }
10889
10901
  },
10890
- "node_modules/get-caller-file": {
10891
- "version": "1.0.3",
10892
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
10893
- "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
10894
- "dev": true
10895
- },
10896
10902
  "node_modules/get-intrinsic": {
10897
10903
  "version": "1.1.1",
10898
10904
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
@@ -14770,31 +14776,6 @@
14770
14776
  "node": ">=12.0.0"
14771
14777
  }
14772
14778
  },
14773
- "node_modules/mock-require": {
14774
- "version": "3.0.3",
14775
- "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz",
14776
- "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==",
14777
- "dev": true,
14778
- "dependencies": {
14779
- "get-caller-file": "^1.0.2",
14780
- "normalize-path": "^2.1.1"
14781
- },
14782
- "engines": {
14783
- "node": ">=4.3.0"
14784
- }
14785
- },
14786
- "node_modules/mock-require/node_modules/normalize-path": {
14787
- "version": "2.1.1",
14788
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
14789
- "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
14790
- "dev": true,
14791
- "dependencies": {
14792
- "remove-trailing-separator": "^1.0.1"
14793
- },
14794
- "engines": {
14795
- "node": ">=0.10.0"
14796
- }
14797
- },
14798
14779
  "node_modules/module-definition": {
14799
14780
  "version": "3.3.1",
14800
14781
  "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.3.1.tgz",
@@ -15052,9 +15033,9 @@
15052
15033
  }
15053
15034
  },
15054
15035
  "node_modules/netlify-onegraph-internal": {
15055
- "version": "0.0.15",
15056
- "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.15.tgz",
15057
- "integrity": "sha512-PlUro29zk/D+9/8NLQOx8Qw58KWvZo/42uR+7YMqSx9pMtgvtqKWqpAGGf0iCy4eAwe/XLT/eCQR6k/dIBbZHw==",
15036
+ "version": "0.0.16",
15037
+ "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.16.tgz",
15038
+ "integrity": "sha512-reQ/C7ztbYCDMXqFLSw0rBwOi866sqdBjagUs6Ug6LXDkCIGHBTjMX0iwNhEn7+/WzV4f1lJj66NhdjF5Q4/aQ==",
15058
15039
  "dependencies": {
15059
15040
  "graphql": "16.0.0",
15060
15041
  "node-fetch": "^2.6.0",
@@ -23445,9 +23426,9 @@
23445
23426
  "integrity": "sha512-tiKmDcHM2riSVN79c0mJY/67EBDafXQAMitHuLiCDAMdtz3kfv+NqdVG5krgf5lWR8Uf8AeZrUW5Q9RP25REvw=="
23446
23427
  },
23447
23428
  "@netlify/eslint-config-node": {
23448
- "version": "4.1.6",
23449
- "resolved": "https://registry.npmjs.org/@netlify/eslint-config-node/-/eslint-config-node-4.1.6.tgz",
23450
- "integrity": "sha512-G/6bqpeR8jQj1cbEjFg8sdc23Wq4TUz/fD+XFup0EYEJbUnjriClsljgMUSnmnBNllyMy36T9YEXkam4HUKtJA==",
23429
+ "version": "4.1.7",
23430
+ "resolved": "https://registry.npmjs.org/@netlify/eslint-config-node/-/eslint-config-node-4.1.7.tgz",
23431
+ "integrity": "sha512-3XYVE0Wnr8YmSTls7447fT4Lyr5goFJ1kK/xevdLVdlzinxd1Wc6SK4um5hZAvl6nrURmgRfLBSIqdpDeRFIlg==",
23451
23432
  "dev": true,
23452
23433
  "requires": {
23453
23434
  "@babel/core": "^7.13.8",
@@ -24813,6 +24794,11 @@
24813
24794
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
24814
24795
  "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
24815
24796
  },
24797
+ "ansi2html": {
24798
+ "version": "0.0.1",
24799
+ "resolved": "https://registry.npmjs.org/ansi2html/-/ansi2html-0.0.1.tgz",
24800
+ "integrity": "sha1-u4gARhtECvALkb89c2al4LhHO6g="
24801
+ },
24816
24802
  "any-observable": {
24817
24803
  "version": "0.3.0",
24818
24804
  "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
@@ -29735,12 +29721,6 @@
29735
29721
  "node-source-walk": "^4.0.0"
29736
29722
  }
29737
29723
  },
29738
- "get-caller-file": {
29739
- "version": "1.0.3",
29740
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
29741
- "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
29742
- "dev": true
29743
- },
29744
29724
  "get-intrinsic": {
29745
29725
  "version": "1.1.1",
29746
29726
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
@@ -32621,27 +32601,6 @@
32621
32601
  "integrity": "sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A==",
32622
32602
  "dev": true
32623
32603
  },
32624
- "mock-require": {
32625
- "version": "3.0.3",
32626
- "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz",
32627
- "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==",
32628
- "dev": true,
32629
- "requires": {
32630
- "get-caller-file": "^1.0.2",
32631
- "normalize-path": "^2.1.1"
32632
- },
32633
- "dependencies": {
32634
- "normalize-path": {
32635
- "version": "2.1.1",
32636
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
32637
- "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
32638
- "dev": true,
32639
- "requires": {
32640
- "remove-trailing-separator": "^1.0.1"
32641
- }
32642
- }
32643
- }
32644
- },
32645
32604
  "module-definition": {
32646
32605
  "version": "3.3.1",
32647
32606
  "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.3.1.tgz",
@@ -32840,9 +32799,9 @@
32840
32799
  }
32841
32800
  },
32842
32801
  "netlify-onegraph-internal": {
32843
- "version": "0.0.15",
32844
- "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.15.tgz",
32845
- "integrity": "sha512-PlUro29zk/D+9/8NLQOx8Qw58KWvZo/42uR+7YMqSx9pMtgvtqKWqpAGGf0iCy4eAwe/XLT/eCQR6k/dIBbZHw==",
32802
+ "version": "0.0.16",
32803
+ "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.16.tgz",
32804
+ "integrity": "sha512-reQ/C7ztbYCDMXqFLSw0rBwOi866sqdBjagUs6Ug6LXDkCIGHBTjMX0iwNhEn7+/WzV4f1lJj66NhdjF5Q4/aQ==",
32846
32805
  "requires": {
32847
32806
  "graphql": "16.0.0",
32848
32807
  "node-fetch": "^2.6.0",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "8.13.2",
4
+ "version": "8.15.0",
5
5
  "author": "Netlify Inc.",
6
6
  "contributors": [
7
7
  "Mathias Biilmann <matt@netlify.com> (https://twitter.com/biilmann)",
@@ -64,7 +64,7 @@
64
64
  "test:dev:ava": "ava --verbose",
65
65
  "test:ci:ava": "c8 -r json ava",
66
66
  "test:affected": "node ./tools/affected-test.js",
67
- "e2e": "node ./tools/e2e/run.js",
67
+ "e2e": "node ./tools/e2e/run.mjs",
68
68
  "docs": "node ./site/scripts/docs.js",
69
69
  "watch": "c8 --reporter=lcov ava --watch",
70
70
  "site:build": "run-s site:build:*",
@@ -73,8 +73,8 @@
73
73
  "postinstall": "node ./scripts/postinstall.js"
74
74
  },
75
75
  "config": {
76
- "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,tools,scripts,site,tests,.github}/**/*.{js,md,html}\" \"*.{js,md,html}\" \".*.{js,md,html}\"",
77
- "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tools,scripts,site,tests,.github}/**/*.{js,md,yml,json,html}\" \"*.{js,yml,json,html}\" \".*.{js,yml,json,html}\" \"!CHANGELOG.md\" \"!npm-shrinkwrap.json\" \"!.github/**/*.md\""
76
+ "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,html}\" \"*.{mjs,cjs,js,md,html}\" \".*.{mjs,cjs,js,md,html}\"",
77
+ "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,yml,json,html}\" \"*.{mjs,cjs,js,yml,json,html}\" \".*.{mjs,cjs,js,yml,json,html}\" \"!CHANGELOG.md\" \"!npm-shrinkwrap.json\" \"!.github/**/*.md\""
78
78
  },
79
79
  "dependencies": {
80
80
  "@netlify/build": "^26.2.0",
@@ -89,6 +89,7 @@
89
89
  "@sindresorhus/slugify": "^1.1.0",
90
90
  "ansi-escapes": "^5.0.0",
91
91
  "ansi-styles": "^5.0.0",
92
+ "ansi2html": "^0.0.1",
92
93
  "ascii-table": "0.0.9",
93
94
  "backoff": "^2.5.0",
94
95
  "better-opn": "^3.0.0",
@@ -103,6 +104,7 @@
103
104
  "content-type": "^1.0.4",
104
105
  "cookie": "^0.4.0",
105
106
  "copy-template-dir": "^1.4.0",
107
+ "cron-parser": "^4.2.1",
106
108
  "debug": "^4.1.1",
107
109
  "decache": "^4.6.0",
108
110
  "del": "^6.0.0",
@@ -146,7 +148,7 @@
146
148
  "multiparty": "^4.2.1",
147
149
  "netlify": "^10.1.2",
148
150
  "netlify-headers-parser": "^6.0.1",
149
- "netlify-onegraph-internal": "0.0.15",
151
+ "netlify-onegraph-internal": "0.0.16",
150
152
  "netlify-redirect-parser": "^13.0.1",
151
153
  "netlify-redirector": "^0.2.1",
152
154
  "node-fetch": "^2.6.0",
@@ -185,7 +187,7 @@
185
187
  },
186
188
  "devDependencies": {
187
189
  "@babel/preset-react": "^7.12.13",
188
- "@netlify/eslint-config-node": "^4.1.6",
190
+ "@netlify/eslint-config-node": "^4.1.7",
189
191
  "ava": "^4.0.0",
190
192
  "c8": "^7.11.0",
191
193
  "eslint-plugin-sort-destructure-keys": "^1.3.5",
@@ -197,7 +199,6 @@
197
199
  "ini": "^2.0.0",
198
200
  "jsonwebtoken": "^8.5.1",
199
201
  "mock-fs": "^5.1.2",
200
- "mock-require": "^3.0.3",
201
202
  "p-timeout": "^4.0.0",
202
203
  "proxyquire": "^2.1.3",
203
204
  "seedrandom": "^3.0.5",
@@ -308,7 +308,7 @@ const dev = async (options, command) => {
308
308
  } else if (startNetlifyGraphWatcher) {
309
309
  const netlifyToken = await command.authenticate()
310
310
  await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id)
311
- const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options })
311
+ const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })
312
312
 
313
313
  log(`Starting Netlify Graph session, to edit your library run \`netlify graph:edit\` in another tab`)
314
314
 
@@ -3,10 +3,11 @@ const fs = require('fs')
3
3
  const path = require('path')
4
4
  const process = require('process')
5
5
 
6
+ const CronParser = require('cron-parser')
6
7
  const inquirer = require('inquirer')
7
8
  const fetch = require('node-fetch')
8
9
 
9
- const { BACKGROUND, NETLIFYDEVWARN, chalk, error, exit, getFunctions } = require('../../utils')
10
+ const { BACKGROUND, CLOCKWORK_USERAGENT, NETLIFYDEVWARN, chalk, error, exit, getFunctions } = require('../../utils')
10
11
 
11
12
  // https://www.netlify.com/docs/functions/#event-triggered-functions
12
13
  const events = [
@@ -130,6 +131,13 @@ const getFunctionToTrigger = function (options, argumentName) {
130
131
  return argumentName
131
132
  }
132
133
 
134
+ const getNextRun = function (schedule) {
135
+ const cron = CronParser.parseExpression(schedule, {
136
+ tz: 'Etc/UTC',
137
+ })
138
+ return cron.next().toDate()
139
+ }
140
+
133
141
  /**
134
142
  * The functions:invoke command
135
143
  * @param {string} nameArgument
@@ -150,11 +158,18 @@ const functionsInvoke = async (nameArgument, options, command) => {
150
158
 
151
159
  const functions = await getFunctions(functionsDir)
152
160
  const functionToTrigger = await getNameFromArgs(functions, options, nameArgument)
161
+ const functionObj = functions.find((func) => func.name === functionToTrigger)
153
162
 
154
163
  let headers = {}
155
164
  let body = {}
156
165
 
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)) {
158
173
  /** handle event triggered fns */
159
174
  // https://www.netlify.com/docs/functions/#event-triggered-functions
160
175
  const [name, event] = functionToTrigger.split('-')
@@ -276,16 +276,13 @@ const link = async (options, command) => {
276
276
  return exit()
277
277
  }
278
278
 
279
- // If already linked to site. exit and prompt for unlink
280
279
  if (siteData) {
280
+ // If already linked to site. exit and prompt for unlink
281
281
  log(`Site already linked to "${siteData.name}"`)
282
282
  log(`Admin url: ${siteData.admin_url}`)
283
283
  log()
284
284
  log(`To unlink this site, run: ${chalk.cyanBright('netlify unlink')}`)
285
- return exit()
286
- }
287
-
288
- if (options.id) {
285
+ } else if (options.id) {
289
286
  try {
290
287
  // @ts-ignore types from API are wrong they cannot recognize `getSite` of API
291
288
  siteData = await api.getSite({ site_id: options.id })
@@ -306,11 +303,7 @@ const link = async (options, command) => {
306
303
  linkType: 'manual',
307
304
  kind: 'byId',
308
305
  })
309
-
310
- return exit()
311
- }
312
-
313
- if (options.name) {
306
+ } else if (options.name) {
314
307
  let results
315
308
  try {
316
309
  results = await listSites({
@@ -341,11 +334,9 @@ const link = async (options, command) => {
341
334
  linkType: 'manual',
342
335
  kind: 'byName',
343
336
  })
344
-
345
- return exit()
337
+ } else {
338
+ siteData = await linkPrompt(command.netlify, options)
346
339
  }
347
-
348
- siteData = await linkPrompt(command.netlify, options)
349
340
  return siteData
350
341
  }
351
342
 
@@ -9,6 +9,7 @@ const { v4: uuidv4 } = require('uuid')
9
9
 
10
10
  const { chalk, error, getRepoData, log, logJson, track, warn } = require('../../utils')
11
11
  const { configureRepo } = require('../../utils/init/config')
12
+ const { link } = require('../link')
12
13
 
13
14
  const SITE_NAME_SUGGESTION_SUFFIX_LENGTH = 5
14
15
 
@@ -159,6 +160,11 @@ const sitesCreate = async (options, command) => {
159
160
  )
160
161
  }
161
162
 
163
+ if (!options.disableLinking) {
164
+ log()
165
+ await link({ id: site.id }, command)
166
+ }
167
+
162
168
  return site
163
169
  }
164
170
 
@@ -172,15 +178,16 @@ const createSitesCreateCommand = (program) =>
172
178
  .command('sites:create')
173
179
  .description(
174
180
  `Create an empty site (advanced)
175
- Create a blank site that isn't associated with any git remote. Does not link to the current working directory.`,
181
+ Create a blank site that isn't associated with any git remote. Will link the site to the current working directory.`,
176
182
  )
177
183
  .option('-n, --name [name]', 'name of site')
178
184
  .option('-a, --account-slug [slug]', 'account slug to create the site under')
179
185
  .option('-c, --with-ci', 'initialize CI hooks during site creation')
180
186
  .option('-m, --manual', 'force manual CI setup. Used --with-ci flag')
187
+ .option('--disable-linking', 'create the site without linking it to current directory')
181
188
  .addHelpText(
182
189
  'after',
183
- `Create a blank site that isn't associated with any git remote. Does not link to the current working directory.`,
190
+ `Create a blank site that isn't associated with any git remote. Will link the site to the current working directory.`,
184
191
  )
185
192
  .action(sitesCreate)
186
193
 
@@ -32,6 +32,7 @@ class NetlifyFunction {
32
32
  // Determines whether this is a background function based on the function
33
33
  // name.
34
34
  this.isBackground = name.endsWith(BACKGROUND_SUFFIX)
35
+ this.schedule = null
35
36
 
36
37
  // List of the function's source files. This starts out as an empty set
37
38
  // and will get populated on every build.
@@ -44,6 +45,12 @@ class NetlifyFunction {
44
45
  return /^[A-Za-z0-9_-]+$/.test(this.name)
45
46
  }
46
47
 
48
+ async isScheduled() {
49
+ await this.buildQueue
50
+
51
+ return Boolean(this.schedule)
52
+ }
53
+
47
54
  // The `build` method transforms source files into invocable functions. Its
48
55
  // return value is an object with:
49
56
  //
@@ -61,12 +68,13 @@ class NetlifyFunction {
61
68
  this.buildQueue = buildFunction({ cache })
62
69
 
63
70
  try {
64
- const { srcFiles, ...buildData } = await this.buildQueue
71
+ const { schedule, srcFiles, ...buildData } = await this.buildQueue
65
72
  const srcFilesSet = new Set(srcFiles)
66
73
  const srcFilesDiff = this.getSrcFilesDiff(srcFilesSet)
67
74
 
68
75
  this.buildData = buildData
69
76
  this.srcFiles = srcFilesSet
77
+ this.schedule = schedule
70
78
 
71
79
  return { srcFilesDiff }
72
80
  } catch (error) {
@@ -1,7 +1,7 @@
1
1
  const { mkdir, writeFile } = require('fs').promises
2
2
  const path = require('path')
3
3
 
4
- const { zipFunction } = require('@netlify/zip-it-and-ship-it')
4
+ const { listFunction, zipFunction } = require('@netlify/zip-it-and-ship-it')
5
5
  const decache = require('decache')
6
6
  const readPkgUp = require('read-pkg-up')
7
7
  const sourceMapSupport = require('source-map-support')
@@ -35,7 +35,11 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
35
35
  // root of the functions directory (e.g. `functions/my-func.js`). In
36
36
  // this case, we use `mainFile` as the function path of `zipFunction`.
37
37
  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({
39
43
  cache,
40
44
  cacheKey: `zisi-${entryPath}`,
41
45
  command: () => zipFunction(entryPath, targetDirectory, zipOptions),
@@ -56,7 +60,22 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr
56
60
 
57
61
  clearFunctionsCache(targetDirectory)
58
62
 
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
60
79
  }
61
80
 
62
81
  // Clears the cache for any files inside the directory from which functions are
@@ -79,10 +98,11 @@ const getTargetDirectory = async ({ errorExit }) => {
79
98
  return targetDirectory
80
99
  }
81
100
 
101
+ const netlifyConfigToZisiConfig = ({ config, projectRoot }) =>
102
+ addFunctionsConfigDefaults(normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot }))
103
+
82
104
  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 })
86
106
 
87
107
  const packageJson = await readPkgUp(func.mainFile)
88
108
  const hasTypeModule = packageJson && packageJson.packageJson.type === 'module'
@@ -115,3 +135,5 @@ module.exports = async ({ config, directory, errorExit, func, projectRoot }) =>
115
135
  target: targetDirectory,
116
136
  }
117
137
  }
138
+
139
+ module.exports.parseForSchedule = parseForSchedule
@@ -46,8 +46,9 @@ const getBuildFunction = async ({ config, directory, errorExit, func, projectRoo
46
46
  // main file otherwise.
47
47
  const functionDirectory = dirname(func.mainFile)
48
48
  const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory]
49
+ const schedule = await detectZisiBuilder.parseForSchedule({ mainFile: func.mainFile, config, projectRoot })
49
50
 
50
- return () => ({ srcFiles })
51
+ return () => ({ schedule, srcFiles })
51
52
  }
52
53
 
53
54
  const invokeFunction = async ({ context, event, func, timeout }) => {
@@ -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 }
@@ -6,6 +6,7 @@ const { NETLIFYDEVERR, NETLIFYDEVLOG, error: errorExit, getInternalFunctionsDir,
6
6
  const { handleBackgroundFunction, handleBackgroundFunctionResult } = require('./background')
7
7
  const { createFormSubmissionHandler } = require('./form-submissions-handler')
8
8
  const { FunctionsRegistry } = require('./registry')
9
+ const { handleScheduledFunction } = require('./scheduled')
9
10
  const { handleSynchronousFunction } = require('./synchronous')
10
11
  const { shouldBase64Encode } = require('./utils')
11
12
 
@@ -105,6 +106,15 @@ const createHandler = function ({ functionsRegistry }) {
105
106
  const { error } = await func.invoke(event, clientContext)
106
107
 
107
108
  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
+ })
108
118
  } else {
109
119
  const { error, result } = await func.invoke(event, clientContext)
110
120
 
@@ -29,7 +29,7 @@ const filterRelativePathItems = (items) => items.filter((part) => part !== '')
29
29
  * @param {import('../base-command').BaseCommand} command
30
30
  * @return {NetlifyGraphConfig} NetlifyGraphConfig
31
31
  */
32
- const getNetlifyGraphConfig = async ({ command, options }) => {
32
+ const getNetlifyGraphConfig = async ({ command, options, settings }) => {
33
33
  const { config, site } = command.netlify
34
34
  config.dev = { ...config.dev }
35
35
  config.build = { ...config.build }
@@ -44,11 +44,12 @@ const getNetlifyGraphConfig = async ({ command, options }) => {
44
44
  }
45
45
 
46
46
  /** @type {Partial<import('../../utils/types').ServerSettings>} */
47
- let settings = {}
48
- try {
49
- settings = await detectServerSettings(devConfig, options, site.root)
50
- } catch (detectServerSettingsError) {
51
- error(detectServerSettingsError)
47
+ if (!settings) {
48
+ try {
49
+ settings = await detectServerSettings(devConfig, options, site.root)
50
+ } catch (detectServerSettingsError) {
51
+ error(detectServerSettingsError)
52
+ }
52
53
  }
53
54
 
54
55
  const siteRoot = [path.sep, ...filterRelativePathItems(site.root.split(path.sep))]
@@ -0,0 +1,5 @@
1
+ const CLOCKWORK_USERAGENT = 'Netlify Clockwork'
2
+
3
+ module.exports = {
4
+ CLOCKWORK_USERAGENT,
5
+ }
@@ -5,10 +5,10 @@ const getUrlPath = (functionName) => `/.netlify/functions/${functionName}`
5
5
 
6
6
  const BACKGROUND = '-background'
7
7
 
8
- const addFunctionProps = ({ mainFile, name, runtime }) => {
8
+ const addFunctionProps = ({ mainFile, name, runtime, schedule }) => {
9
9
  const urlPath = getUrlPath(name)
10
10
  const isBackground = name.endsWith(BACKGROUND)
11
- return { mainFile, name, runtime, urlPath, isBackground }
11
+ return { mainFile, name, runtime, urlPath, isBackground, schedule }
12
12
  }
13
13
 
14
14
  const JS = 'js'
@@ -21,7 +21,9 @@ const getFunctions = async (functionsSrcDir) => {
21
21
  // performance optimization, load '@netlify/zip-it-and-ship-it' on demand
22
22
  // eslint-disable-next-line node/global-require
23
23
  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
+ })
25
27
  const functionsWithProps = functions.filter(({ runtime }) => runtime === JS).map((func) => addFunctionProps(func))
26
28
  return functionsWithProps
27
29
  }
@@ -1,8 +1,10 @@
1
+ const constants = require('./constants')
1
2
  const edgeHandlers = require('./edge-handlers')
2
3
  const functions = require('./functions')
3
4
  const getFunctions = require('./get-functions')
4
5
 
5
6
  module.exports = {
7
+ ...constants,
6
8
  ...functions,
7
9
  ...edgeHandlers,
8
10
  ...getFunctions,