methanol 0.0.0 → 0.0.2

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.
Files changed (48) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +58 -0
  3. package/banner.txt +6 -0
  4. package/bin/methanol.js +24 -0
  5. package/index.js +22 -0
  6. package/package.json +51 -9
  7. package/src/assets.js +30 -0
  8. package/src/build-system.js +200 -0
  9. package/src/components.js +145 -0
  10. package/src/config.js +396 -0
  11. package/src/dev-server.js +632 -0
  12. package/src/main.js +133 -0
  13. package/src/mdx.js +406 -0
  14. package/src/node-loader.js +88 -0
  15. package/src/pagefind.js +107 -0
  16. package/src/pages.js +771 -0
  17. package/src/preview-server.js +58 -0
  18. package/src/public-assets.js +73 -0
  19. package/src/register-loader.js +29 -0
  20. package/src/rehype-plugins/link-resolve.js +116 -0
  21. package/src/rehype-plugins/methanol-ctx.js +89 -0
  22. package/src/renderer.js +25 -0
  23. package/src/rewind.js +117 -0
  24. package/src/stage-logger.js +59 -0
  25. package/src/state.js +179 -0
  26. package/src/virtual-module/inject.js +30 -0
  27. package/src/virtual-module/loader.js +116 -0
  28. package/src/virtual-module/pagefind.js +108 -0
  29. package/src/vite-plugins.js +173 -0
  30. package/themes/default/components/ThemeAccentSwitch.client.jsx +95 -0
  31. package/themes/default/components/ThemeAccentSwitch.static.jsx +23 -0
  32. package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
  33. package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
  34. package/themes/default/components/ThemeSearchBox.client.jsx +324 -0
  35. package/themes/default/components/ThemeSearchBox.static.jsx +40 -0
  36. package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
  37. package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
  38. package/themes/default/components/pre.client.jsx +84 -0
  39. package/themes/default/components/pre.static.jsx +27 -0
  40. package/themes/default/heading.jsx +35 -0
  41. package/themes/default/index.js +41 -0
  42. package/themes/default/page.jsx +303 -0
  43. package/themes/default/pages/404.mdx +8 -0
  44. package/themes/default/pages/index.mdx +31 -0
  45. package/themes/default/public/favicon.png +0 -0
  46. package/themes/default/public/logo.png +0 -0
  47. package/themes/default/sources/prefetch.js +49 -0
  48. package/themes/default/sources/style.css +1660 -0
@@ -0,0 +1,632 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ import { existsSync } from 'fs'
22
+ import { readFile } from 'fs/promises'
23
+ import { resolve, dirname, extname, join, basename, relative } from 'path'
24
+ import { fileURLToPath } from 'url'
25
+ import chokidar from 'chokidar'
26
+ import { createServer, mergeConfig } from 'vite'
27
+ import { refurbish } from 'refurbish/vite'
28
+ import { state, cli } from './state.js'
29
+ import { resolveUserViteConfig } from './config.js'
30
+ import {
31
+ buildComponentRegistry,
32
+ buildComponentEntry,
33
+ invalidateRegistryEntry,
34
+ bumpComponentImportNonce,
35
+ isComponentFile,
36
+ isClientComponent,
37
+ COMPONENT_EXTENSIONS
38
+ } from './components.js'
39
+ import { buildPagesContext, buildPageEntry, routePathFromFile } from './pages.js'
40
+ import { compilePageMdx, renderHtml } from './mdx.js'
41
+ import { methanolResolverPlugin } from './vite-plugins.js'
42
+ import { copyPublicDir } from './public-assets.js'
43
+
44
+ export const runViteDev = async () => {
45
+ const baseFsAllow = [state.ROOT_DIR, state.USER_THEME.root].filter(Boolean)
46
+ const baseConfig = {
47
+ configFile: false,
48
+ root: state.PAGES_DIR,
49
+ publicDir: state.STATIC_DIR === false ? false : state.STATIC_DIR,
50
+ server: {
51
+ fs: {
52
+ allow: baseFsAllow
53
+ }
54
+ },
55
+ esbuild: {
56
+ jsx: 'automatic',
57
+ jsxImportSource: 'refui'
58
+ },
59
+ resolve: {
60
+ dedupe: ['refui', 'methanol']
61
+ },
62
+ plugins: [methanolResolverPlugin(), refurbish()]
63
+ }
64
+ const userConfig = await resolveUserViteConfig('serve')
65
+ const finalConfig = userConfig ? mergeConfig(baseConfig, userConfig) : baseConfig
66
+ if (state.STATIC_DIR !== false) {
67
+ await copyPublicDir({
68
+ sourceDir: state.THEME_PUBLIC_DIR,
69
+ targetDir: state.STATIC_DIR,
70
+ label: 'theme public'
71
+ })
72
+ }
73
+ if (cli.CLI_PORT != null) {
74
+ finalConfig.server = { ...(finalConfig.server || {}), port: cli.CLI_PORT }
75
+ }
76
+ if (cli.CLI_HOST !== null) {
77
+ finalConfig.server = { ...(finalConfig.server || {}), host: cli.CLI_HOST }
78
+ }
79
+ if (baseFsAllow.length) {
80
+ const fsConfig = finalConfig.server?.fs || {}
81
+ const allow = Array.isArray(fsConfig.allow) ? fsConfig.allow : []
82
+ for (const dir of baseFsAllow) {
83
+ if (!allow.includes(dir)) {
84
+ allow.push(dir)
85
+ }
86
+ }
87
+ finalConfig.server = {
88
+ ...(finalConfig.server || {}),
89
+ fs: {
90
+ ...fsConfig,
91
+ allow
92
+ }
93
+ }
94
+ }
95
+ const server = await createServer(finalConfig)
96
+
97
+ const themeComponentsDir = state.THEME_COMPONENTS_DIR
98
+ const themeEnv = state.THEME_ENV
99
+ const themeRegistry = themeComponentsDir
100
+ ? await buildComponentRegistry({
101
+ componentsDir: themeComponentsDir,
102
+ client: themeEnv.client
103
+ })
104
+ : { components: {} }
105
+ const themeComponents = {
106
+ ...(themeRegistry.components || {}),
107
+ ...(state.USER_THEME.components || {})
108
+ }
109
+ const initialRegistry = await buildComponentRegistry()
110
+ let components = initialRegistry.components
111
+ const componentSources = new Map(initialRegistry.sources)
112
+ let pagesContext = null
113
+ let pagesContextToken = 0
114
+ const setPagesContext = (next) => {
115
+ pagesContext = next
116
+ pagesContextToken += 1
117
+ }
118
+ setPagesContext(await buildPagesContext({ compileAll: false }))
119
+ const htmlCache = new Map()
120
+ let htmlCacheEpoch = 0
121
+
122
+ const invalidateHtmlCache = () => {
123
+ htmlCacheEpoch += 1
124
+ htmlCache.clear()
125
+ }
126
+
127
+ const refreshPagesContext = async () => {
128
+ setPagesContext(await buildPagesContext({ compileAll: false }))
129
+ }
130
+
131
+ const runInitialCompile = async () => {
132
+ const token = pagesContextToken
133
+ try {
134
+ const nextContext = await buildPagesContext({ compileAll: true })
135
+ if (token !== pagesContextToken) {
136
+ return
137
+ }
138
+ setPagesContext(nextContext)
139
+ invalidateHtmlCache()
140
+ reload()
141
+ } catch (err) {
142
+ console.error(err)
143
+ }
144
+ }
145
+
146
+ const resolvePageFile = (routePath) => {
147
+ const name = routePath === '/' ? 'index' : routePath.slice(1)
148
+ const mdxPath = resolve(state.PAGES_DIR, `${name}.mdx`)
149
+ if (existsSync(mdxPath)) return mdxPath
150
+ const mdPath = resolve(state.PAGES_DIR, `${name}.md`)
151
+ if (existsSync(mdPath)) return mdPath
152
+ return mdxPath
153
+ }
154
+
155
+ const resolveHtmlCandidates = (pathname) => {
156
+ const candidates = []
157
+ if (pathname === '/' || pathname === '') {
158
+ candidates.push('/index.html')
159
+ } else if (pathname.endsWith('.html')) {
160
+ candidates.push(pathname)
161
+ } else {
162
+ candidates.push(`${pathname}.html`)
163
+ candidates.push(`${pathname}/index.html`)
164
+ }
165
+ return candidates.map((candidate) =>
166
+ resolve(state.PAGES_DIR, candidate.replace(/^\//, ''))
167
+ )
168
+ }
169
+
170
+ const shouldServeHtml = (relativePath, requestedPath, hasMdx) => {
171
+ if (hasMdx) return false
172
+ const baseName = basename(relativePath, '.html')
173
+ if (baseName.startsWith('_') || baseName.startsWith('.')) return false
174
+ const excludedDirs = pagesContext?.excludedDirs
175
+ if (excludedDirs?.size) {
176
+ const dir = relativePath.split('/').slice(0, -1).join('/')
177
+ for (const excludedDir of excludedDirs) {
178
+ if (!excludedDir) return false
179
+ if (dir === excludedDir || dir.startsWith(`${excludedDir}/`)) {
180
+ return false
181
+ }
182
+ }
183
+ }
184
+ const excludedRoutes = pagesContext?.excludedRoutes
185
+ if (excludedRoutes?.has(requestedPath)) return false
186
+ const excludedDirPaths = pagesContext?.excludedDirPaths
187
+ if (excludedDirPaths?.size) {
188
+ for (const dirPath of excludedDirPaths) {
189
+ if (requestedPath === dirPath || requestedPath.startsWith(`${dirPath}/`)) {
190
+ return false
191
+ }
192
+ }
193
+ }
194
+ return true
195
+ }
196
+
197
+ const htmlMiddleware = async (req, res, next) => {
198
+ if (!req.url || req.method !== 'GET') {
199
+ return next()
200
+ }
201
+ if (req.url.startsWith('/@vite') || req.url.startsWith('/__vite')) {
202
+ return next()
203
+ }
204
+
205
+ const url = new URL(req.url, 'http://methanol')
206
+ let pathname = url.pathname
207
+ try {
208
+ pathname = decodeURIComponent(pathname)
209
+ } catch {}
210
+ const originalPathname = pathname
211
+ const hasTrailingSlash = originalPathname.endsWith('/') && originalPathname !== '/'
212
+
213
+ if (pathname.includes('.') && !pathname.endsWith('.html')) {
214
+ return next()
215
+ }
216
+
217
+ const accept = req.headers.accept || ''
218
+ if (!pathname.endsWith('.html') && !accept.includes('text/html')) {
219
+ return next()
220
+ }
221
+
222
+ let routePath = pathname
223
+ if (routePath.endsWith('.html')) {
224
+ routePath = routePath.slice(0, -'.html'.length)
225
+ if (routePath === '') {
226
+ routePath = '/'
227
+ }
228
+ }
229
+ if (routePath.endsWith('/') && routePath !== '/') {
230
+ routePath = routePath.slice(0, -1)
231
+ }
232
+
233
+ const requestedPath = routePath
234
+ const isExcludedPath = () => {
235
+ const excludedRoutes = pagesContext?.excludedRoutes
236
+ if (excludedRoutes?.has(requestedPath)) return true
237
+ const excludedDirPaths = pagesContext?.excludedDirPaths
238
+ if (excludedDirPaths?.size) {
239
+ for (const dirPath of excludedDirPaths) {
240
+ if (requestedPath === dirPath || requestedPath.startsWith(`${dirPath}/`)) {
241
+ return true
242
+ }
243
+ }
244
+ }
245
+ return false
246
+ }
247
+ const notFoundPage = pagesContext?.pagesByRoute?.get('/404') ?? null
248
+ const pageMeta = hasTrailingSlash
249
+ ? (pagesContext?.pagesByRouteIndex?.get(requestedPath) ?? pagesContext?.pagesByRoute?.get(requestedPath) ?? null)
250
+ : (pagesContext?.pagesByRoute?.get(requestedPath) ?? null)
251
+ let filePath = pageMeta?.filePath || resolvePageFile(requestedPath)
252
+ const hasMdx = Boolean(pageMeta) || existsSync(filePath)
253
+ let status = 200
254
+ let renderRoutePath = requestedPath
255
+
256
+ if (!hasMdx) {
257
+ const candidates = resolveHtmlCandidates(pathname)
258
+ for (const candidate of candidates) {
259
+ if (!existsSync(candidate)) continue
260
+ const relativePath = relative(state.PAGES_DIR, candidate).replace(/\\/g, '/')
261
+ if (relativePath.startsWith('..')) {
262
+ continue
263
+ }
264
+ if (!shouldServeHtml(relativePath, requestedPath, hasMdx)) {
265
+ continue
266
+ }
267
+ try {
268
+ const html = await readFile(candidate, 'utf-8')
269
+ const candidateUrl = `/${relativePath}`
270
+ const transformed = await server.transformIndexHtml(candidateUrl, html)
271
+ res.statusCode = 200
272
+ res.setHeader('Content-Type', 'text/html')
273
+ res.end(transformed)
274
+ return
275
+ } catch (err) {
276
+ console.error(err)
277
+ res.statusCode = 500
278
+ res.end('Internal Server Error')
279
+ return
280
+ }
281
+ }
282
+ }
283
+
284
+ if (isExcludedPath()) {
285
+ if (notFoundPage) {
286
+ filePath = notFoundPage.filePath
287
+ renderRoutePath = '/404'
288
+ status = 404
289
+ } else {
290
+ return next()
291
+ }
292
+ } else if (requestedPath === '/404' && notFoundPage) {
293
+ filePath = notFoundPage.filePath
294
+ renderRoutePath = '/404'
295
+ status = 404
296
+ } else if (!pageMeta && !existsSync(filePath)) {
297
+ if (notFoundPage) {
298
+ filePath = notFoundPage.filePath
299
+ renderRoutePath = '/404'
300
+ status = 404
301
+ } else {
302
+ return next()
303
+ }
304
+ }
305
+
306
+ try {
307
+ const renderEpoch = htmlCacheEpoch
308
+ const cacheEntry = htmlCache.get(renderRoutePath)
309
+ if (cacheEntry && cacheEntry.filePath === filePath && cacheEntry.epoch === htmlCacheEpoch) {
310
+ res.statusCode = status
311
+ res.setHeader('Content-Type', 'text/html')
312
+ res.end(cacheEntry.html)
313
+ return
314
+ }
315
+
316
+ const html = await renderHtml({
317
+ routePath: renderRoutePath,
318
+ filePath,
319
+ components: {
320
+ ...themeComponents,
321
+ ...components
322
+ },
323
+ pagesContext,
324
+ pageMeta
325
+ })
326
+ if (renderEpoch === htmlCacheEpoch) {
327
+ htmlCache.set(renderRoutePath, {
328
+ html,
329
+ filePath,
330
+ epoch: renderEpoch
331
+ })
332
+ }
333
+ res.statusCode = status
334
+ res.setHeader('Content-Type', 'text/html')
335
+ res.end(html)
336
+ } catch (err) {
337
+ console.error(err)
338
+ res.statusCode = 500
339
+ res.end('Internal Server Error')
340
+ }
341
+ }
342
+
343
+ if (Array.isArray(server.middlewares.stack)) {
344
+ server.middlewares.stack.unshift({ route: '', handle: htmlMiddleware })
345
+ } else {
346
+ server.middlewares.use(htmlMiddleware)
347
+ }
348
+
349
+ await server.listen()
350
+ server.printUrls()
351
+
352
+ const invalidateRewindInject = () => {
353
+ const registryModule = server.moduleGraph.getModuleById('\0/.methanol_virtual_module/registry.js')
354
+ if (registryModule) {
355
+ server.moduleGraph.invalidateModule(registryModule)
356
+ }
357
+ const injectModule = server.moduleGraph.getModuleById('\0/.methanol_virtual_module/inject.js')
358
+ if (injectModule) {
359
+ server.moduleGraph.invalidateModule(injectModule)
360
+ }
361
+ }
362
+
363
+ let queue = Promise.resolve()
364
+ const enqueue = (task) => {
365
+ queue = queue.then(task).catch((err) => {
366
+ console.error(err)
367
+ })
368
+ return queue
369
+ }
370
+
371
+ const reload = () => {
372
+ server.ws.send({ type: 'full-reload' })
373
+ }
374
+
375
+ runInitialCompile()
376
+
377
+ const refreshPages = async () => {
378
+ await refreshPagesContext()
379
+ invalidateHtmlCache()
380
+ reload()
381
+ }
382
+
383
+ const getExportName = (filePath) => basename(filePath).split('.')[0]
384
+
385
+ const findComponentExt = (dir, exportName) => {
386
+ for (const ext of COMPONENT_EXTENSIONS) {
387
+ if (
388
+ existsSync(join(dir, `${exportName}${ext}`)) ||
389
+ existsSync(join(dir, `${exportName}.client${ext}`)) ||
390
+ existsSync(join(dir, `${exportName}.static${ext}`))
391
+ ) {
392
+ return ext
393
+ }
394
+ }
395
+ return null
396
+ }
397
+
398
+ const updateComponentEntry = async (filePath, { fallback = false } = {}) => {
399
+ bumpComponentImportNonce()
400
+ const exportName = getExportName(filePath)
401
+ const dir = dirname(filePath)
402
+ let ext = extname(filePath)
403
+ let { component, hasClient, staticPath } = await buildComponentEntry({
404
+ dir,
405
+ exportName,
406
+ ext
407
+ })
408
+ if (!component && fallback) {
409
+ ext = findComponentExt(dir, exportName)
410
+ if (ext) {
411
+ ;({ component, hasClient, staticPath } = await buildComponentEntry({
412
+ dir,
413
+ exportName,
414
+ ext
415
+ }))
416
+ }
417
+ }
418
+ if (!component) {
419
+ delete components[exportName]
420
+ componentSources.delete(exportName)
421
+ invalidateRegistryEntry(exportName)
422
+ return { hasClient: false }
423
+ }
424
+ if (!hasClient) {
425
+ invalidateRegistryEntry(exportName)
426
+ }
427
+ components[exportName] = component
428
+ if (staticPath) {
429
+ componentSources.set(exportName, staticPath)
430
+ }
431
+ return { hasClient }
432
+ }
433
+
434
+ const pageWatchPaths = [state.PAGES_DIR, state.THEME_PAGES_DIR].filter(Boolean)
435
+ const pageWatcher = chokidar.watch(pageWatchPaths, {
436
+ ignoreInitial: true
437
+ })
438
+
439
+ const PAGE_UPDATE_DEBOUNCE_MS = 30
440
+ const pageUpdateTimers = new Map()
441
+
442
+ const schedulePageUpdate = (filePath, kind) => {
443
+ const existing = pageUpdateTimers.get(filePath)
444
+ if (existing?.timer) {
445
+ clearTimeout(existing.timer)
446
+ }
447
+ const entry = {
448
+ kind,
449
+ timer: setTimeout(() => {
450
+ pageUpdateTimers.delete(filePath)
451
+ enqueue(() => handlePageUpdate(filePath, kind))
452
+ }, PAGE_UPDATE_DEBOUNCE_MS)
453
+ }
454
+ pageUpdateTimers.set(filePath, entry)
455
+ }
456
+
457
+ const resolveWatchedSource = (filePath) => {
458
+ const inUserPages = routePathFromFile(filePath, state.PAGES_DIR)
459
+ if (inUserPages) {
460
+ return { pagesDir: state.PAGES_DIR, source: 'user', routePath: inUserPages }
461
+ }
462
+ if (state.THEME_PAGES_DIR) {
463
+ const inThemePages = routePathFromFile(filePath, state.THEME_PAGES_DIR)
464
+ if (inThemePages) {
465
+ return { pagesDir: state.THEME_PAGES_DIR, source: 'theme', routePath: inThemePages }
466
+ }
467
+ }
468
+ return null
469
+ }
470
+
471
+ const updatePageEntry = async (filePath, resolved) => {
472
+ if (!pagesContext || !resolved) return false
473
+ pagesContext.clearDerivedTitle?.(filePath)
474
+ const nextEntry = await buildPageEntry({
475
+ filePath,
476
+ pagesDir: resolved.pagesDir,
477
+ source: resolved.source
478
+ })
479
+ if (!nextEntry) return false
480
+ const prevEntry = pagesContext.pages?.find?.((page) => page.filePath === filePath) || null
481
+ if (!prevEntry) return false
482
+ if (prevEntry.exclude !== nextEntry.exclude) return false
483
+ if (prevEntry.isIndex !== nextEntry.isIndex || prevEntry.dir !== nextEntry.dir) return false
484
+ Object.assign(prevEntry, nextEntry)
485
+ prevEntry.mdxComponent = null
486
+ prevEntry.toc = null
487
+ pagesContext.refreshPagesTree?.()
488
+ pagesContext.refreshLanguages?.()
489
+ if (prevEntry.content && prevEntry.content.trim().length) {
490
+ await compilePageMdx(prevEntry, pagesContext, {
491
+ lazyPagesTree: true,
492
+ refreshPagesTree: false
493
+ })
494
+ // Avoid caching a potentially stale render; recompile on request.
495
+ prevEntry.mdxComponent = null
496
+ }
497
+ return true
498
+ }
499
+
500
+ const isUserHeadAsset = (filePath) => {
501
+ const name = basename(filePath)
502
+ if (name !== 'style.css' && name !== 'index.js' && name !== 'index.ts') {
503
+ return false
504
+ }
505
+ const root = resolve(state.PAGES_DIR || '')
506
+ return root && resolve(dirname(filePath)) === root
507
+ }
508
+
509
+ const handlePageUpdate = async (filePath, kind) => {
510
+ if (isUserHeadAsset(filePath)) {
511
+ invalidateHtmlCache()
512
+ reload()
513
+ return
514
+ }
515
+ const resolved = resolveWatchedSource(filePath)
516
+ if (kind === 'unlink') {
517
+ if (resolved?.routePath) {
518
+ htmlCache.delete(resolved.routePath)
519
+ } else {
520
+ return
521
+ }
522
+ await refreshPages()
523
+ return
524
+ }
525
+ const updated = await updatePageEntry(filePath, resolved)
526
+ if (updated) {
527
+ invalidateHtmlCache()
528
+ reload()
529
+ return
530
+ }
531
+ if (!resolved?.routePath) {
532
+ return
533
+ }
534
+ await refreshPages()
535
+ }
536
+
537
+ pageWatcher.on('change', (filePath) => {
538
+ schedulePageUpdate(filePath, 'change')
539
+ })
540
+
541
+ pageWatcher.on('add', (filePath) => {
542
+ schedulePageUpdate(filePath, 'add')
543
+ })
544
+ pageWatcher.on('unlink', (filePath) => {
545
+ schedulePageUpdate(filePath, 'unlink')
546
+ })
547
+ pageWatcher.on('addDir', () => {
548
+ enqueue(refreshPages)
549
+ })
550
+
551
+ pageWatcher.on('unlinkDir', () => {
552
+ enqueue(refreshPages)
553
+ })
554
+
555
+ const componentWatcher = chokidar.watch(state.COMPONENTS_DIR, {
556
+ ignoreInitial: true
557
+ })
558
+
559
+ componentWatcher.on('add', (filePath) => {
560
+ if (!isComponentFile(filePath)) {
561
+ return
562
+ }
563
+ if (isClientComponent(filePath)) {
564
+ enqueue(async () => {
565
+ const { hasClient } = await updateComponentEntry(filePath)
566
+ if (hasClient) {
567
+ invalidateRewindInject()
568
+ }
569
+ invalidateHtmlCache()
570
+ reload()
571
+ })
572
+ return
573
+ }
574
+ enqueue(async () => {
575
+ const { hasClient } = await updateComponentEntry(filePath)
576
+ invalidateHtmlCache()
577
+ if (hasClient) {
578
+ invalidateRewindInject()
579
+ }
580
+ reload()
581
+ })
582
+ })
583
+
584
+ componentWatcher.on('change', (filePath) => {
585
+ if (!isComponentFile(filePath)) {
586
+ return
587
+ }
588
+ if (isClientComponent(filePath)) {
589
+ enqueue(async () => {
590
+ await updateComponentEntry(filePath)
591
+ invalidateHtmlCache()
592
+ })
593
+ return
594
+ }
595
+ enqueue(async () => {
596
+ const { hasClient } = await updateComponentEntry(filePath)
597
+ invalidateHtmlCache()
598
+ if (hasClient) {
599
+ invalidateRewindInject()
600
+ }
601
+ reload()
602
+ })
603
+ })
604
+
605
+ componentWatcher.on('unlink', (filePath) => {
606
+ if (!isComponentFile(filePath)) return
607
+ if (isClientComponent(filePath)) {
608
+ enqueue(async () => {
609
+ await updateComponentEntry(filePath, { fallback: true })
610
+ invalidateRewindInject()
611
+ invalidateHtmlCache()
612
+ reload()
613
+ })
614
+ return
615
+ }
616
+ const exportName = getExportName(filePath)
617
+ const currentSource = componentSources.get(exportName)
618
+ if (currentSource && currentSource !== filePath && existsSync(currentSource)) {
619
+ return
620
+ }
621
+ enqueue(async () => {
622
+ const { hasClient } = await updateComponentEntry(filePath, {
623
+ fallback: true
624
+ })
625
+ invalidateHtmlCache()
626
+ if (hasClient) {
627
+ invalidateRewindInject()
628
+ }
629
+ reload()
630
+ })
631
+ })
632
+ }