Skip to content

fix: use separate watcher script for middleware in dev #1831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0eb9eb1
fix: watch middleware in src folder
ascorbic Dec 2, 2022
7b1bc45
fix: move watcher into subprocess
ascorbic Dec 5, 2022
d34718d
chore: add comments
ascorbic Dec 5, 2022
b94f132
chore: use join
ascorbic Dec 5, 2022
848faee
Merge branch 'main' into mk/dev-watch
ascorbic Dec 5, 2022
bc0c49d
chore: changes from review
ascorbic Dec 6, 2022
3058c24
feat: add support for running watcher once
ascorbic Dec 6, 2022
6795c91
chore: add onPreDev tests
ascorbic Dec 6, 2022
6cab2cc
chore: add watcher tests
ascorbic Dec 6, 2022
6562611
chore: add more tests
ascorbic Dec 6, 2022
5013442
chore: snap
ascorbic Dec 6, 2022
82f5f71
chore: remove duplicates
ascorbic Dec 6, 2022
c61caf0
chore: run jest serially
ascorbic Dec 6, 2022
514cb1d
chore: skip the watcher test
ascorbic Dec 6, 2022
4dac4a2
chore: re-enable some tests
ascorbic Dec 6, 2022
5d6a551
chore: enable more
ascorbic Dec 6, 2022
bd8bf3d
chore: wait to kill processes
ascorbic Dec 6, 2022
75181f7
chore: re-enable tests
ascorbic Dec 6, 2022
1f58588
chore: try and make it testable
ascorbic Dec 7, 2022
839771b
chore: just one worker
ascorbic Dec 7, 2022
e20dfe3
chore: disable predev test
ascorbic Dec 7, 2022
0f9e28b
chore: so much logging
ascorbic Dec 7, 2022
7bbf3ed
Merge branch 'main' into mk/dev-watch
ericapisani Dec 8, 2022
ff45167
Merge remote-tracking branch 'origin/main' into mk/dev-watch
nickytonline Mar 17, 2023
8b8ed08
chore: added some comments and renamed some functions
nickytonline Mar 17, 2023
559b48d
Update test/index.spec.js
nickytonline Mar 17, 2023
29dad6f
chore: removed some white space
nickytonline Mar 20, 2023
f88a68d
chore: fixed tests
nickytonline Mar 20, 2023
3d26c71
test: skipping some tests for onPreDev middleware as they only run su…
nickytonline Mar 20, 2023
713a740
chore: removed console.logs that were for debugging
nickytonline Mar 20, 2023
ab838e9
chore: added another test to skip for now
nickytonline Mar 20, 2023
c64bb95
Merge branch 'main' into mk/dev-watch
nickytonline Mar 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 24 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@netlify/ipx": "^1.3.1",
"@vercel/node-bridge": "^2.1.0",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"destr": "^1.1.1",
"execa": "^5.1.1",
"follow-redirects": "^1.15.2",
Expand Down
42 changes: 5 additions & 37 deletions packages/runtime/src/helpers/dev.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import type { Buffer } from 'buffer'
import { resolve } from 'path'
import { Transform } from 'stream'
import { join } from 'path'

import { OnPreBuild } from '@netlify/build'
import type { OnPreBuild } from '@netlify/build'
import execa from 'execa'
import { unlink } from 'fs-extra'
import mergeStream from 'merge-stream'

import { writeDevEdgeFunction } from './edge'
import { patchNextFiles } from './files'
Expand All @@ -17,37 +13,9 @@ export const onPreDev: OnPreBuild = async ({ constants, netlifyConfig }) => {
// Need to patch the files, because build might not have been run
await patchNextFiles(base)

// Clean up old functions
await unlink(resolve('.netlify', 'middleware.js')).catch(() => {
// Ignore if it doesn't exist
})
await writeDevEdgeFunction(constants)

// Eventually we might want to do this via esbuild's API, but for now the CLI works fine
const common = [`--bundle`, `--outdir=${resolve('.netlify')}`, `--format=esm`, `--target=esnext`, '--watch']
const opts = {
all: true,
env: { ...process.env, FORCE_COLOR: '1' },
}
// TypeScript
const tsout = execa(`esbuild`, [...common, resolve(base, 'middleware.ts')], opts).all

// JavaScript
const jsout = execa(`esbuild`, [...common, resolve(base, 'middleware.js')], opts).all

const filter = new Transform({
transform(chunk: Buffer, encoding, callback) {
const str = chunk.toString(encoding)

// Skip if message includes this, because we run even when the files are missing
if (!str.includes('[ERROR] Could not resolve')) {
this.push(chunk)
}
callback()
},
// Don't await this or it will never finish
execa.node(join(__dirname, 'watcher.js'), [base], {
stdio: 'inherit',
})

mergeStream(tsout, jsout).pipe(filter).pipe(process.stdout)

// Don't return the promise because we don't want to wait for the child process to finish
}
91 changes: 91 additions & 0 deletions packages/runtime/src/helpers/watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { promises } from 'fs'
import { join } from 'path'

import { build } from '@netlify/esbuild'
import { watch } from 'chokidar'

const fileList = (watched: Record<string, Array<string>>) =>
Object.entries(watched).flatMap(([dir, files]) => files.map((file) => join(dir, file)))

const start = async (base: string) => {
const watcher = watch(['middleware.js', 'middleware.ts', 'src/middleware.js', 'src/middleware.ts'], {
// Try and ensure renames just emit one event
atomic: true,
// Don't emit for every watched file, just once after the scan is done
ignoreInitial: true,
cwd: base,
})

const update = async (initial = false) => {
try {
// Start by deleting the old file. If we error out, we don't want to leave the old file around
await promises.unlink(join(base, '.netlify', 'middleware.js'))
} catch {
// Ignore, because it's fine if it didn't exist
}
// The list of watched files is an object with the directory as the key and an array of files as the value.
// We need to flatten this into a list of files
const watchedFiles = fileList(watcher.getWatched())
if (watchedFiles.length === 0) {
if (!initial) {
// Only log on subsequent builds, because having it on first build makes it seem like a warning, when it's a normal state
console.log('No middleware found')
}
return
}
if (watchedFiles.length > 1) {
console.log('Multiple middleware files found:')
console.log(watchedFiles.join('\n'))
console.log('This is not supported.')
// Return without compiling anything
return
}
console.log(`${initial ? 'Building' : 'Rebuilding'} middleware ${watchedFiles[0]}...`)
try {
await build({
entryPoints: watchedFiles,
outfile: join(base, '.netlify', 'middleware.js'),
bundle: true,
format: 'esm',
target: 'esnext',
})
} catch (error) {
console.error(error)
return
}

console.log('...done')
}

Choose a reason for hiding this comment

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

Reading the contents of the update method, I think this could be broken down into several helper functions and then have them be invoked within update:

removeOldMiddlewareFile:

try {
      // Start by deleting the old file. If we error out, we don't want to leave the old file around
      await promises.unlink(join(base, '.netlify', 'middleware.js'))
    } catch {
      // Ignore, because it's fine if it didn't exist
    }

The logging in the branches of watchedFiles.length === 0 and watchedFiles.length > 1 could be it's own distinct method, with the return statement happening after that logging is done if necessary.

buildMiddlewareFile:

    try {
      await build({
        entryPoints: watchedFiles,
        outfile: join(base, '.netlify', 'middleware.js'),
        bundle: true,
        format: 'esm',
        target: 'esnext',
      })
    } catch (error) {
      console.error(error)
      return
    }


try {
watcher
.on('change', (path) => {
console.log(`File ${path} has been changed`)
update()
})
.on('add', (path) => {
console.log(`File ${path} has been added`)
update()
})
.on('unlink', (path) => {
console.log(`File ${path} has been removed`)
update()
})
.on('ready', () => {
console.log('Initial scan complete. Ready for changes')
// This only happens on the first scan
update(true)
})
await new Promise((resolve) => {
watcher.on('close', resolve)
})
} finally {
await watcher.close()
}
}

start(process.argv[2]).catch((error) => {
console.error(error)
// eslint-disable-next-line n/no-process-exit
process.exit(1)
})