Skip to content

Commit 3de5008

Browse files
feat: serve bundled functions in ntl dev (#2094)
* feat: serve bundled functions in ntl dev * feat: use function builder API * feat: add omitFileChangesLog property * fix: use errorExit instead of process.exit * feat: use zisi builder only when esbuild is enabled * chore: add function builder tests * chore: remove warning message
1 parent 6ad9444 commit 3de5008

File tree

6 files changed

+252
-39
lines changed

6 files changed

+252
-39
lines changed

package-lock.json

Lines changed: 29 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/dev/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ class DevCommand extends Command {
249249
}
250250

251251
await startFunctionsServer({
252+
config,
252253
settings,
253254
site,
254255
log,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const path = require('path')
2+
3+
const { zipFunction, zipFunctions } = require('@netlify/zip-it-and-ship-it')
4+
const makeDir = require('make-dir')
5+
6+
const { getPathInProject } = require('../lib/settings')
7+
const { NETLIFYDEVERR } = require('../utils/logo')
8+
9+
const bundleFunctions = ({ config, sourceDirectory, targetDirectory, updatedPath }) => {
10+
// If `updatedPath` is truthy, it means we're running the build command due
11+
// to an update to a file. If that's the case, we run `zipFunction` to bundle
12+
// that specific function only.
13+
if (updatedPath) {
14+
return zipFunction(updatedPath, targetDirectory, {
15+
archiveFormat: 'none',
16+
config,
17+
})
18+
}
19+
20+
return zipFunctions(sourceDirectory, targetDirectory, {
21+
archiveFormat: 'none',
22+
config,
23+
})
24+
}
25+
26+
// The function configuration keys returned by @netlify/config are not an exact
27+
// match to the properties that @netlify/zip-it-and-ship-it expects. We do that
28+
// translation here.
29+
const normalizeFunctionsConfig = (functionsConfig = {}) =>
30+
Object.entries(functionsConfig).reduce(
31+
(result, [pattern, config]) => ({
32+
...result,
33+
[pattern]: {
34+
externalNodeModules: config.external_node_modules,
35+
ignoredNodeModules: config.ignored_node_modules,
36+
nodeBundler: config.node_bundler === 'esbuild' ? 'esbuild_zisi' : config.node_bundler,
37+
},
38+
}),
39+
{},
40+
)
41+
42+
const getTargetDirectory = async ({ errorExit }) => {
43+
const targetDirectory = path.resolve(getPathInProject(['functions-serve']))
44+
45+
try {
46+
await makeDir(targetDirectory)
47+
} catch (error) {
48+
errorExit(`${NETLIFYDEVERR} Could not create directory: ${targetDirectory}`)
49+
}
50+
51+
return targetDirectory
52+
}
53+
54+
module.exports = async function handler({ config, errorExit, functionsDirectory: sourceDirectory }) {
55+
const functionsConfig = normalizeFunctionsConfig(config.functions)
56+
const isUsingEsbuild = functionsConfig['*'] && functionsConfig['*'].nodeBundler === 'esbuild_zisi'
57+
58+
if (!isUsingEsbuild) {
59+
return false
60+
}
61+
62+
const targetDirectory = await getTargetDirectory({ errorExit })
63+
64+
return {
65+
build: (updatedPath) => bundleFunctions({ config: functionsConfig, sourceDirectory, targetDirectory, updatedPath }),
66+
builderName: 'zip-it-and-ship-it',
67+
omitFileChangesLog: true,
68+
src: sourceDirectory,
69+
target: targetDirectory,
70+
}
71+
}

src/utils/detect-functions-builder.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
const fs = require('fs')
22
const path = require('path')
33

4-
const detectFunctionsBuilder = async function (projectDir) {
4+
const detectFunctionsBuilder = async function (parameters) {
55
const detectors = fs
66
.readdirSync(path.join(__dirname, '..', 'function-builder-detectors'))
77
// only accept .js detector files
88
.filter((filename) => filename.endsWith('.js'))
9+
// Sorting by filename
10+
.sort()
911
// eslint-disable-next-line node/global-require, import/no-dynamic-require
1012
.map((det) => require(path.join(__dirname, '..', `function-builder-detectors/${det}`)))
1113

1214
for (const detector of detectors) {
1315
// eslint-disable-next-line no-await-in-loop
14-
const settings = await detector(projectDir)
16+
const settings = await detector(parameters)
1517
if (settings) {
1618
return settings
1719
}

src/utils/serve-functions.js

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const { getLogMessage } = require('../lib/log')
2020

2121
const { detectFunctionsBuilder } = require('./detect-functions-builder')
2222
const { getFunctions } = require('./get-functions')
23-
const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR } = require('./logo')
23+
const { NETLIFYDEVLOG, NETLIFYDEVERR } = require('./logo')
2424

2525
const formatLambdaLocalError = (err) => `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace.join('\n ')}`
2626

@@ -153,12 +153,18 @@ const buildClientContext = function (headers) {
153153
}
154154
}
155155

156-
const clearCache = (action) => (path) => {
157-
console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`)
156+
const clearCache = ({ action, omitLog }) => (path) => {
157+
if (!omitLog) {
158+
console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`)
159+
}
160+
158161
Object.keys(require.cache).forEach((key) => {
159162
delete require.cache[key]
160163
})
161-
console.log(`${NETLIFYDEVLOG} ${path} ${action}, successfully reloaded!`)
164+
165+
if (!omitLog) {
166+
console.log(`${NETLIFYDEVLOG} ${path} ${action}, successfully reloaded!`)
167+
}
162168
}
163169

164170
const shouldBase64Encode = function (contentType) {
@@ -173,11 +179,13 @@ const validateFunctions = function ({ functions, capabilities, warn }) {
173179
}
174180
}
175181

176-
const createHandler = async function ({ dir, capabilities, warn }) {
182+
const createHandler = async function ({ dir, capabilities, omitFileChangesLog, warn }) {
177183
const functions = await getFunctions(dir)
178184
validateFunctions({ functions, capabilities, warn })
179185
const watcher = chokidar.watch(dir, { ignored: /node_modules/ })
180-
watcher.on('change', clearCache('modified')).on('unlink', clearCache('deleted'))
186+
watcher
187+
.on('change', clearCache({ action: 'modified', omitLog: omitFileChangesLog }))
188+
.on('unlink', clearCache({ action: 'deleted', omitLog: omitFileChangesLog }))
181189

182190
const logger = winston.createLogger({
183191
levels: winston.config.npm.levels,
@@ -363,7 +371,7 @@ const createFormSubmissionHandler = function ({ siteUrl, warn }) {
363371
}
364372
}
365373

366-
const getFunctionsServer = async function ({ dir, siteUrl, capabilities, warn }) {
374+
const getFunctionsServer = async function ({ dir, omitFileChangesLog, siteUrl, capabilities, warn }) {
367375
const app = express()
368376
app.set('query parser', 'simple')
369377

@@ -385,21 +393,21 @@ const getFunctionsServer = async function ({ dir, siteUrl, capabilities, warn })
385393
res.status(204).end()
386394
})
387395

388-
app.all('*', await createHandler({ dir, capabilities, warn }))
396+
app.all('*', await createHandler({ dir, capabilities, omitFileChangesLog, warn }))
389397

390398
return app
391399
}
392400

393401
const getBuildFunction = ({ functionBuilder, log }) =>
394-
async function build() {
402+
async function build(updatedPath) {
395403
log(
396404
`${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} ${chalk.magenta(
397405
'building',
398406
)} functions from directory ${chalk.yellow(functionBuilder.src)}`,
399407
)
400408

401409
try {
402-
await functionBuilder.build()
410+
await functionBuilder.build(updatedPath)
403411
log(
404412
`${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} ${chalk.green(
405413
'finished',
@@ -417,32 +425,40 @@ const getBuildFunction = ({ functionBuilder, log }) =>
417425
}
418426
}
419427

420-
const setupFunctionsBuilder = async ({ site, log, warn }) => {
421-
const functionBuilder = await detectFunctionsBuilder(site.root)
422-
if (functionBuilder) {
423-
log(
424-
`${NETLIFYDEVLOG} Function builder ${chalk.yellow(
425-
functionBuilder.builderName,
426-
)} detected: Running npm script ${chalk.yellow(functionBuilder.npmScript)}`,
427-
)
428-
warn(
429-
`${NETLIFYDEVWARN} This is a beta feature, please give us feedback on how to improve at https://github.com/netlify/cli/`,
430-
)
428+
const setupFunctionsBuilder = async ({ config, errorExit, functionsDirectory, log, site }) => {
429+
const functionBuilder = await detectFunctionsBuilder({
430+
config,
431+
errorExit,
432+
functionsDirectory,
433+
log,
434+
projectRoot: site.root,
435+
})
431436

432-
const debouncedBuild = debounce(getBuildFunction({ functionBuilder, log }), 300, {
433-
leading: true,
434-
trailing: true,
435-
})
437+
if (!functionBuilder) {
438+
return {}
439+
}
436440

437-
await debouncedBuild()
441+
const npmScriptString = functionBuilder.npmScript
442+
? `: Running npm script ${chalk.yellow(functionBuilder.npmScript)}`
443+
: ''
438444

439-
const functionWatcher = chokidar.watch(functionBuilder.src)
440-
functionWatcher.on('ready', () => {
441-
functionWatcher.on('add', debouncedBuild)
442-
functionWatcher.on('change', debouncedBuild)
443-
functionWatcher.on('unlink', debouncedBuild)
444-
})
445-
}
445+
log(`${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} detected${npmScriptString}.`)
446+
447+
const debouncedBuild = debounce(getBuildFunction({ functionBuilder, log }), 300, {
448+
leading: true,
449+
trailing: true,
450+
})
451+
452+
await debouncedBuild()
453+
454+
const functionWatcher = chokidar.watch(functionBuilder.src)
455+
functionWatcher.on('ready', () => {
456+
functionWatcher.on('add', debouncedBuild)
457+
functionWatcher.on('change', debouncedBuild)
458+
functionWatcher.on('unlink', debouncedBuild)
459+
})
460+
461+
return functionBuilder
446462
}
447463

448464
const startServer = async ({ server, settings, log, errorExit }) => {
@@ -458,12 +474,25 @@ const startServer = async ({ server, settings, log, errorExit }) => {
458474
})
459475
}
460476

461-
const startFunctionsServer = async ({ settings, site, log, warn, errorExit, siteUrl, capabilities }) => {
477+
const startFunctionsServer = async ({ config, settings, site, log, warn, errorExit, siteUrl, capabilities }) => {
462478
// serve functions from zip-it-and-ship-it
463479
// env variables relies on `url`, careful moving this code
464480
if (settings.functions) {
465-
await setupFunctionsBuilder({ site, log, warn })
466-
const server = await getFunctionsServer({ dir: settings.functions, siteUrl, capabilities, warn })
481+
const { omitFileChangesLog, target: functionsDirectory } = await setupFunctionsBuilder({
482+
config,
483+
errorExit,
484+
functionsDirectory: settings.functions,
485+
log,
486+
site,
487+
})
488+
const server = await getFunctionsServer({
489+
dir: functionsDirectory || settings.functions,
490+
omitFileChangesLog,
491+
siteUrl,
492+
capabilities,
493+
warn,
494+
})
495+
467496
await startServer({ server, settings, log, errorExit })
468497
}
469498
}

0 commit comments

Comments
 (0)