Ben Schlegel
2023-08-27 c91e62c376d481534d89084e5c04846878dff6d3
quartz/bootstrap-cli.mjs
@@ -43,6 +43,27 @@
  },
}
const CreateArgv = {
  ...CommonArgv,
  source: {
    string: true,
    alias: ["s"],
    describe: "source directory to copy/create symlink from",
  },
  strategy: {
    string: true,
    alias: ["X"],
    choices: ["new", "copy", "symlink"],
    describe: "strategy for content folder setup",
  },
  links: {
    string: true,
    alias: ["l"],
    choices: ["absolute", "shortest", "relative"],
    describe: "strategy to resolve links",
  },
}
const SyncArgv = {
  ...CommonArgv,
  commit: {
@@ -147,25 +168,73 @@
  .scriptName("quartz")
  .version(version)
  .usage("$0 <cmd> [args]")
  .command("create", "Initialize Quartz", CommonArgv, async (argv) => {
  .command("create", "Initialize Quartz", CreateArgv, async (argv) => {
    console.log()
    intro(chalk.bgGreen.black(` Quartz v${version} `))
    const contentFolder = path.join(cwd, argv.directory)
    const setupStrategy = exitIfCancel(
      await select({
        message: `Choose how to initialize the content in \`${contentFolder}\``,
        options: [
          { value: "new", label: "Empty Quartz" },
          { value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" },
          {
            value: "symlink",
            label: "Symlink an existing folder",
            hint: "don't select this unless you know what you are doing!",
          },
          { value: "keep", label: "Keep the existing files" },
        ],
      }),
    )
    let setupStrategy = argv.strategy?.toLowerCase()
    let linkResolutionStrategy = argv.links?.toLowerCase()
    const sourceDirectory = argv.source
    // If all cmd arguments were provided, check if theyre valid
    if (setupStrategy && linkResolutionStrategy) {
      // If setup isn't, "new", source argument is required
      if (setupStrategy !== "new") {
        // Error handling
        if (!sourceDirectory) {
          outro(
            chalk.red(
              `Setup strategies (arg '${chalk.yellow(
                `-${CreateArgv.strategy.alias[0]}`,
              )}') other than '${chalk.yellow(
                "new",
              )}' require content folder argument ('${chalk.yellow(
                `-${CreateArgv.source.alias[0]}`,
              )}') to be set`,
            ),
          )
          process.exit(1)
        } else {
          if (!fs.existsSync(sourceDirectory)) {
            outro(
              chalk.red(
                `Input directory to copy/symlink 'content' from not found ('${chalk.yellow(
                  sourceDirectory,
                )}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`,
              ),
            )
            process.exit(1)
          } else if (!fs.lstatSync(sourceDirectory).isDirectory()) {
            outro(
              chalk.red(
                `Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow(
                  sourceDirectory,
                )}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`,
              ),
            )
            process.exit(1)
          }
        }
      }
    }
    // Use cli process if cmd args werent provided
    if (!setupStrategy) {
      setupStrategy = exitIfCancel(
        await select({
          message: `Choose how to initialize the content in \`${contentFolder}\``,
          options: [
            { value: "new", label: "Empty Quartz" },
            { value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" },
            {
              value: "symlink",
              label: "Symlink an existing folder",
              hint: "don't select this unless you know what you are doing!",
            },
          ],
        }),
      )
    }
    async function rmContentFolder() {
      const contentStat = await fs.promises.lstat(contentFolder)
@@ -176,24 +245,30 @@
      }
    }
    await fs.promises.unlink(path.join(contentFolder, ".gitkeep"))
    if (setupStrategy === "copy" || setupStrategy === "symlink") {
      const originalFolder = escapePath(
        exitIfCancel(
          await text({
            message: "Enter the full path to existing content folder",
            placeholder:
              "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
            validate(fp) {
              const fullPath = escapePath(fp)
              if (!fs.existsSync(fullPath)) {
                return "The given path doesn't exist"
              } else if (!fs.lstatSync(fullPath).isDirectory()) {
                return "The given path is not a folder"
              }
            },
          }),
        ),
      )
      let originalFolder = sourceDirectory
      // If input directory was not passed, use cli
      if (!sourceDirectory) {
        originalFolder = escapePath(
          exitIfCancel(
            await text({
              message: "Enter the full path to existing content folder",
              placeholder:
                "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
              validate(fp) {
                const fullPath = escapePath(fp)
                if (!fs.existsSync(fullPath)) {
                  return "The given path doesn't exist"
                } else if (!fs.lstatSync(fullPath).isDirectory()) {
                  return "The given path is not a folder"
                }
              },
            }),
          ),
        )
      }
      await rmContentFolder()
      if (setupStrategy === "copy") {
@@ -205,8 +280,6 @@
        await fs.promises.symlink(originalFolder, contentFolder, "dir")
      }
    } else if (setupStrategy === "new") {
      await rmContentFolder()
      await fs.promises.mkdir(contentFolder)
      await fs.promises.writeFile(
        path.join(contentFolder, "index.md"),
        `---
@@ -219,29 +292,32 @@
      )
    }
    // get a prefered link resolution strategy
    const linkResolutionStrategy = exitIfCancel(
      await select({
        message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
        options: [
          {
            value: "absolute",
            label: "Treat links as absolute path",
            hint: "for content made for Quartz 3 and Hugo",
          },
          {
            value: "shortest",
            label: "Treat links as shortest path",
            hint: "for most Obsidian vaults",
          },
          {
            value: "relative",
            label: "Treat links as relative paths",
            hint: "for just normal Markdown files",
          },
        ],
      }),
    )
    // Use cli process if cmd args werent provided
    if (!linkResolutionStrategy) {
      // get a preferred link resolution strategy
      linkResolutionStrategy = exitIfCancel(
        await select({
          message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
          options: [
            {
              value: "absolute",
              label: "Treat links as absolute path",
              hint: "for content made for Quartz 3 and Hugo",
            },
            {
              value: "shortest",
              label: "Treat links as shortest path",
              hint: "for most Obsidian vaults",
            },
            {
              value: "relative",
              label: "Treat links as relative paths",
              hint: "for just normal Markdown files",
            },
          ],
        }),
      )
    }
    // now, do config changes
    const configFilePath = path.join(cwd, "quartz.config.ts")
@@ -255,7 +331,7 @@
    outro(`You're all set! Not sure what to do next? Try:
   • Customizing Quartz a bit more by editing \`quartz.config.ts\`
   • Running \`npx quartz build --serve\` to preview your Quartz locally
   • Hosting your Quartz online (see: https://quartz.jzhao.xyz/setup/hosting)
   • Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting)
`)
  })
  .command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => {
@@ -393,14 +469,28 @@
    })
    const buildMutex = new Mutex()
    const timeoutIds = new Set()
    let lastBuildMs = 0
    let cleanupBuild = null
    const build = async (clientRefresh) => {
      await buildMutex.acquire()
      const buildStart = new Date().getTime()
      lastBuildMs = buildStart
      const release = await buildMutex.acquire()
      if (lastBuildMs > buildStart) {
        release()
        return
      }
      if (cleanupBuild) {
        await cleanupBuild()
        console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
      }
      const result = await ctx.rebuild().catch((err) => {
        console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
        console.log(`Reason: ${chalk.grey(err)}`)
        process.exit(1)
      })
      release()
      if (argv.bundleInfo) {
        const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
@@ -416,15 +506,8 @@
      // bypass module cache
      // https://github.com/nodejs/modules/issues/307
      const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`)
      await buildQuartz(argv, clientRefresh)
      cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh)
      clientRefresh()
      buildMutex.release()
    }
    const rebuild = (clientRefresh) => {
      timeoutIds.forEach((id) => clearTimeout(id))
      timeoutIds.clear()
      timeoutIds.add(setTimeout(() => build(clientRefresh), 250))
    }
    if (argv.serve) {
@@ -452,14 +535,22 @@
        req.url = req.url?.slice(argv.baseDir.length)
        const serve = async () => {
          const release = await buildMutex.acquire()
          await serveHandler(req, res, {
            public: argv.output,
            directoryListing: false,
            headers: [
              {
                source: "**/*.html",
                headers: [{ key: "Content-Disposition", value: "inline" }],
              },
            ],
          })
          const status = res.statusCode
          const statusString =
            status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
          console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`))
          release()
        }
        const redirect = (newFp) => {
@@ -526,8 +617,7 @@
          ignoreInitial: true,
        })
        .on("all", async () => {
          console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
          rebuild(clientRefresh)
          build(clientRefresh)
        })
    } else {
      await build(() => {})