Jacky Zhao
2023-07-24 55a1fb8c41b6a34cc5cdfc5ec2533ed1f642211d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import "source-map-support/register.js"
import path from "path"
import { PerfTimer } from "./perf"
import { rimraf } from "rimraf"
import { globby, isGitIgnored } from "globby"
import chalk from "chalk"
import http from "http"
import serveHandler from "serve-handler"
import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit"
import cfg from "../quartz.config"
import { FilePath } from "./path"
import chokidar from "chokidar"
import { ProcessedContent } from "./plugins/vfile"
import WebSocket, { WebSocketServer } from "ws"
import { Argv, BuildCtx } from "./ctx"
 
async function buildQuartz(argv: Argv, version: string) {
  const ctx: BuildCtx = {
    argv,
    cfg,
  }
 
  console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
  const perf = new PerfTimer()
  const output = argv.output
 
  const pluginCount = Object.values(cfg.plugins).flat().length
  const pluginNames = (key: "transformers" | "filters" | "emitters") =>
    cfg.plugins[key].map((plugin) => plugin.name)
  if (argv.verbose) {
    console.log(`Loaded ${pluginCount} plugins`)
    console.log(`  Transformers: ${pluginNames("transformers").join(", ")}`)
    console.log(`  Filters: ${pluginNames("filters").join(", ")}`)
    console.log(`  Emitters: ${pluginNames("emitters").join(", ")}`)
  }
 
  perf.addEvent("clean")
  await rimraf(output)
  console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
 
  perf.addEvent("glob")
  const fps = await globby("**/*.md", {
    cwd: argv.directory,
    ignore: cfg.configuration.ignorePatterns,
    gitignore: true,
  })
  console.log(
    `Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
  )
 
  const filePaths = fps.map((fp) => `${argv.directory}${path.sep}${fp}` as FilePath)
  const parsedFiles = await parseMarkdown(ctx, filePaths)
  const filteredContent = filterContent(ctx, parsedFiles)
  await emitContent(ctx, filteredContent)
  console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
 
  if (argv.serve) {
    await startServing(ctx, parsedFiles)
  }
}
 
async function startServing(ctx: BuildCtx, initialContent: ProcessedContent[]) {
  const { argv } = ctx
  const wss = new WebSocketServer({ port: 3001 })
  const connections: WebSocket[] = []
  wss.on("connection", (ws) => connections.push(ws))
 
  const ignored = await isGitIgnored()
  const contentMap = new Map<FilePath, ProcessedContent>()
  for (const content of initialContent) {
    const [_tree, vfile] = content
    contentMap.set(vfile.data.filePath!, content)
  }
 
  async function rebuild(fp: string, action: "add" | "change" | "unlink") {
    const perf = new PerfTimer()
    if (!ignored(fp)) {
      console.log(chalk.yellow(`Detected change in ${fp}, rebuilding...`))
      const fullPath = `${argv.directory}${path.sep}${fp}` as FilePath
 
      try {
        if (action === "add" || action === "change") {
          const [parsedContent] = await parseMarkdown(ctx, [fullPath])
          contentMap.set(fullPath, parsedContent)
        } else if (action === "unlink") {
          contentMap.delete(fullPath)
        }
 
        await rimraf(argv.output)
        const parsedFiles = [...contentMap.values()]
        const filteredContent = filterContent(ctx, parsedFiles)
        await emitContent(ctx, filteredContent)
        console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
      } catch {
        console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
      }
 
      connections.forEach((conn) => conn.send("rebuild"))
    }
  }
 
  const watcher = chokidar.watch(".", {
    persistent: true,
    cwd: argv.directory,
    ignoreInitial: true,
  })
 
  watcher
    .on("add", (fp) => rebuild(fp, "add"))
    .on("change", (fp) => rebuild(fp, "change"))
    .on("unlink", (fp) => rebuild(fp, "unlink"))
 
  const server = http.createServer(async (req, res) => {
    await serveHandler(req, res, {
      public: argv.output,
      directoryListing: false,
    })
    const status = res.statusCode
    const statusString =
      status >= 200 && status < 300
        ? chalk.green(`[${status}]`)
        : status >= 300 && status < 400
        ? chalk.yellow(`[${status}]`)
        : chalk.red(`[${status}]`)
    console.log(statusString + chalk.grey(` ${req.url}`))
  })
  server.listen(argv.port)
  console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`))
  console.log("hint: exit with ctrl+c")
}
 
export default async (argv: Argv, version: string) => {
  try {
    await buildQuartz(argv, version)
  } catch {
    console.log(chalk.red("\nExiting Quartz due to a fatal error"))
    process.exit(1)
  }
}