UNPKG

5.84 kBPlain TextView Raw
1import fs from 'fs-extra'
2// @ts-ignore
3import graph from 'watch-dependency-graph'
4import chokidar from 'chokidar'
5import match from 'picomatch'
6
7import { outputLambdas } from './outputLambdas'
8import * as logger from './log'
9import { getFiles, isStatic, isDynamic } from './getFiles'
10import { renderStaticEntries } from './renderStaticEntries'
11import { timer } from './timer'
12import { createConfig, removeConfigValues, getConfigFile } from './config'
13import { builtStaticFiles } from './builtStaticFiles'
14import { removeBuiltStaticFile } from './removeBuiltStaticFile'
15import { Presta } from './types'
16
17/*
18 * Wraps outputLambdas for logging
19 */
20function updateLambdas(inputs: string[], config: Presta) {
21 const time = timer()
22
23 // always write this, even if inputs = []
24 outputLambdas(inputs, config)
25
26 // if user actually has routes configured, give feedback
27 if (inputs.length) {
28 logger.info({
29 label: 'built',
30 message: `lambdas`,
31 duration: time(),
32 })
33 }
34}
35
36export async function watch(config: Presta) {
37 /*
38 * Get files that match static/dynamic patters at startup
39 */
40 let files = getFiles(config)
41 let hasConfigFile = fs.existsSync(config.configFilepath)
42
43 if (!files.length) {
44 logger.warn({
45 label: 'paths',
46 message: 'no files configured',
47 })
48 }
49
50 /*
51 * Create initial dynamic entry regardless of if the user has routes, bc we
52 * need this file to serve 404 locally
53 */
54 updateLambdas(files.filter(isDynamic), config)
55
56 /*
57 * Set up all watchers
58 */
59 const fileWatcher = graph({ alias: { '@': config.cwd } })
60 const globalWatcher = chokidar.watch(config.cwd, {
61 ignoreInitial: true,
62 ignored: [config.output, config.assets],
63 })
64
65 /*
66 * On a config update, the user may have passed in a new `files` array or
67 * other global config required by all files, so we need to re-fetch all
68 * files and rebuild everything.
69 */
70 async function handleConfigUpdate() {
71 files = getFiles(config)
72 await renderStaticEntries(files.filter(isStatic), config)
73 updateLambdas(files.filter(isDynamic), config)
74 }
75
76 /*
77 * On a changed file, we can just render it
78 */
79 async function handleFileChange(file: string) {
80 // render just file that changed
81 if (isStatic(file)) {
82 await renderStaticEntries([file], config)
83 }
84
85 // update dynamic entry with ALL dynamic files
86 if (isDynamic(file)) {
87 updateLambdas(files.filter(isDynamic), config)
88 }
89
90 config.hooks.emitBrowserRefresh()
91 }
92
93 config.hooks.onBuildFile(({ file }) => {
94 handleFileChange(file)
95 })
96
97 fileWatcher.on('remove', async ([id]: string[]) => {
98 logger.debug({
99 label: 'watch',
100 message: `fileWatcher - removed ${id}`,
101 })
102
103 // remove from local hash
104 files.splice(files.indexOf(id), 1)
105
106 // update this regardless, not sure if [id] was dynamic or static
107 updateLambdas(files.filter(isDynamic), config)
108
109 // if it was config, we gotta do a restart
110 if (id === config.configFilepath) {
111 // filter out values from the config file
112 config = await removeConfigValues()
113
114 // reset this!
115 hasConfigFile = false
116
117 handleConfigUpdate()
118 }
119
120 ;(builtStaticFiles[id] || []).forEach((file) => removeBuiltStaticFile(file, config))
121 })
122
123 fileWatcher.on('change', async ([id]: string[]) => {
124 logger.debug({
125 label: 'watch',
126 message: `fileWatcher - changed ${id}`,
127 })
128
129 if (id === config.configFilepath) {
130 // clear config file for re-require
131 delete require.cache[config.configFilepath]
132
133 try {
134 // merge in new values from config file
135 config = await createConfig({
136 config: getConfigFile(config.configFilepath),
137 })
138
139 handleConfigUpdate()
140 } catch (e) {
141 logger.error({
142 label: 'error',
143 error: e as Error,
144 })
145 }
146 } else {
147 handleFileChange(id)
148 }
149 })
150
151 fileWatcher.on('error', (e: Error) => {
152 logger.error({
153 label: 'error',
154 error: e,
155 })
156 })
157
158 /*
159 * globalWatcher watches the raw file globs passed to the CLI or as `files`
160 * in the config. If checks on add/change to see if a file should be upgraded
161 * to a a Presta source file, and added to the fileWatcher. It also watches
162 * for addition of a config file.
163 */
164 globalWatcher.on('all', async (event, file) => {
165 // ignore events handled by wdg, or any directory events
166 if (!/add|change/.test(event) || !fs.existsSync(file) || fs.lstatSync(file).isDirectory()) return
167
168 // if a file change matches any pages globs
169 if (match(config.files)(file) && !files.includes(file)) {
170 logger.debug({
171 label: 'watch',
172 message: `globalWatcher - add ${file}`,
173 })
174
175 files.push(file)
176
177 fileWatcher.add(file)
178
179 handleFileChange(file)
180 }
181
182 // if file matches config file and we don't already have one
183 if (file === config.configFilepath && !hasConfigFile) {
184 logger.debug({
185 label: 'watch',
186 message: `globalWatcher - add config file ${file}`,
187 })
188
189 fileWatcher.add(config.configFilepath)
190
191 try {
192 // merge in new values from config file
193 config = await createConfig({
194 config: getConfigFile(config.configFilepath),
195 })
196
197 hasConfigFile = true
198
199 handleConfigUpdate()
200 } catch (e) {
201 logger.error({
202 label: 'error',
203 error: e as Error,
204 })
205 }
206 }
207 })
208
209 /**
210 * Init watching after event subscriptions
211 */
212 fileWatcher.add(files)
213 if (hasConfigFile) fileWatcher.add(config.configFilepath)
214
215 /**
216 * Prime files to check for errors on startup and register any plugins
217 */
218 try {
219 files.map(require)
220 } catch (e) {
221 logger.error({
222 label: 'error',
223 error: e as Error,
224 })
225 }
226}
227
\No newline at end of file