Commit bf26083

Anton Golub <antongolub@antongolub.com>
2025-01-11 22:36:12
feat(cli): process md for any kind of input (#1078)
* feat(cli): process md for any kind of input closes #1076 * fix(cli): fix updateArgv block
1 parent 49a82c4
Changed files (4)
src/cli.ts
@@ -26,6 +26,7 @@ import {
   fetch,
   fs,
   path,
+  stdin,
   VERSION,
 } from './index.js'
 import { installDeps, parseDeps } from './deps.js'
@@ -113,100 +114,95 @@ export async function main() {
     await startRepl()
     return
   }
-  if (argv.eval) {
-    await runScript(argv.eval, argv.ext)
-    return
-  }
-  const [firstArg] = argv._
-  updateArgv(argv._.slice(firstArg === undefined ? 0 : 1))
-  if (!firstArg || firstArg === '-') {
-    const success = await scriptFromStdin(argv.ext)
-    if (!success) {
-      printUsage()
-      process.exitCode = 1
-    }
-    return
-  }
-  if (/^https?:/.test(firstArg)) {
-    await scriptFromHttp(firstArg, argv.ext)
-    return
-  }
-  const filepath = firstArg.startsWith('file:')
-    ? url.fileURLToPath(firstArg)
-    : path.resolve(firstArg)
-  await importPath(filepath)
-}
 
-export async function runScript(script: string, ext?: string) {
-  const filepath = getFilepath($.cwd, 'zx', ext)
-  await writeAndImport(script, filepath)
+  const { script, scriptPath, tempPath } = await readScript()
+  await runScript(script, scriptPath, tempPath)
 }
 
-export async function scriptFromStdin(ext?: string): Promise<boolean> {
-  let script = ''
-  if (!process.stdin.isTTY) {
-    process.stdin.setEncoding('utf8')
-    for await (const chunk of process.stdin) {
-      script += chunk
+async function runScript(
+  script: string,
+  scriptPath: string,
+  tempPath: string
+): Promise<void> {
+  const rmTemp = () => fs.rmSync(tempPath, { force: true })
+  try {
+    if (tempPath) {
+      scriptPath = tempPath
+      await fs.writeFile(tempPath, script)
     }
 
-    if (script.length > 0) {
-      await runScript(script, ext)
-      return true
+    if (argv.install) {
+      await installDeps(
+        parseDeps(script),
+        path.dirname(scriptPath),
+        argv.registry
+      )
     }
-  }
-  return false
-}
 
-export async function scriptFromHttp(remote: string, _ext?: string) {
-  const res = await fetch(remote)
-  if (!res.ok) {
-    console.error(`Error: Can't get ${remote}`)
-    process.exit(1)
-  }
-  const script = await res.text()
-  const pathname = new URL(remote).pathname
-  const { name, ext } = path.parse(pathname)
-  const filepath = getFilepath($.cwd, name, _ext || ext)
-  await writeAndImport(script, filepath)
-}
+    injectGlobalRequire(scriptPath)
+    process.once('exit', rmTemp)
 
-export async function writeAndImport(
-  script: string | Buffer,
-  filepath: string,
-  origin = filepath
-) {
-  await fs.writeFile(filepath, script)
-  try {
-    process.once('exit', () => fs.rmSync(filepath, { force: true }))
-    await importPath(filepath, origin)
+    // TODO: fix unanalyzable-dynamic-import to work correctly with jsr.io
+    await import(url.pathToFileURL(scriptPath).toString())
   } finally {
-    await fs.rm(filepath)
+    rmTemp()
   }
 }
 
-export async function importPath(
-  filepath: string,
-  origin = filepath
-): Promise<void> {
-  const contents = await fs.readFile(filepath, 'utf8')
-  const { ext, base, dir } = path.parse(filepath)
-  const tempFilename = getFilepath(dir, base)
+async function readScript() {
+  const [firstArg] = argv._
+  let script = ''
+  let scriptPath = ''
+  let tempPath = ''
+  let argSlice = 1
+
+  if (argv.eval) {
+    argSlice = 0
+    script = argv.eval
+    tempPath = getFilepath($.cwd, 'zx', argv.ext)
+  } else if (!firstArg || firstArg === '-') {
+    script = await readScriptFromStdin()
+    tempPath = getFilepath($.cwd, 'zx', argv.ext)
+    if (script.length === 0) {
+      printUsage()
+      process.exitCode = 1
+      throw new Error('No script provided')
+    }
+  } else if (/^https?:/.test(firstArg)) {
+    const { name, ext = argv.ext } = path.parse(new URL(firstArg).pathname)
+    script = await readScriptFromHttp(firstArg)
+    tempPath = getFilepath($.cwd, name, ext)
+  } else {
+    script = await fs.readFile(firstArg, 'utf8')
+    scriptPath = firstArg.startsWith('file:')
+      ? url.fileURLToPath(firstArg)
+      : path.resolve(firstArg)
+  }
 
+  const { ext, base, dir } = path.parse(tempPath || scriptPath)
   if (ext === '') {
-    return writeAndImport(contents, tempFilename, origin)
+    tempPath = getFilepath(dir, base)
   }
   if (ext === '.md') {
-    return writeAndImport(transformMarkdown(contents), tempFilename, origin)
-  }
-  if (argv.install) {
-    const deps = parseDeps(contents)
-    await installDeps(deps, dir, argv.registry)
+    script = transformMarkdown(script)
+    tempPath = getFilepath(dir, base)
   }
+  if (argSlice) updateArgv(argv._.slice(argSlice))
+
+  return { script, scriptPath, tempPath }
+}
+
+async function readScriptFromStdin(): Promise<string> {
+  return !process.stdin.isTTY ? stdin() : ''
+}
 
-  injectGlobalRequire(origin)
-  // TODO: fix unanalyzable-dynamic-import to work correctly with jsr.io
-  await import(url.pathToFileURL(filepath).toString())
+async function readScriptFromHttp(remote: string): Promise<string> {
+  const res = await fetch(remote)
+  if (!res.ok) {
+    console.error(`Error: Can't get ${remote}`)
+    process.exit(1)
+  }
+  return res.text()
 }
 
 export function injectGlobalRequire(origin: string) {
test/fixtures/md.http
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Content-Type: plain/text; charset=UTF-8
+Content-Length: 15
+Server: netcat!
+
+# Title
+```js
+$`echo 'md'`
+```
test/cli.test.js
@@ -191,7 +191,7 @@ describe('cli', () => {
     const port = await getPort()
     const server = await getServer([resp]).start(port)
     const out =
-      await $`node build/cli.js --verbose http://127.0.0.1:${port}/echo.mjs`
+      await $`node build/cli.js --verbose http://127.0.0.1:${port}/script.mjs`
     assert.match(out.stderr, /test/)
     await server.stop()
   })
@@ -204,6 +204,16 @@ describe('cli', () => {
     await server.stop()
   })
 
+  test('scripts (md) from https', async () => {
+    const resp = await fs.readFile(path.resolve('test/fixtures/md.http'))
+    const port = await getPort()
+    const server = await getServer([resp]).start(port)
+    const out =
+      await $`node build/cli.js --verbose http://127.0.0.1:${port}/script.md`
+    assert.match(out.stderr, /md/)
+    await server.stop()
+  })
+
   test('scripts with no extension', async () => {
     await $`node build/cli.js test/fixtures/no-extension`
     assert.ok(
@@ -237,10 +247,6 @@ describe('cli', () => {
     await $`node build/cli.js test/fixtures/markdown.md`
   })
 
-  test('markdown scripts are working', async () => {
-    await $`node build/cli.js test/fixtures/markdown.md`
-  })
-
   test('markdown scripts are working for CRLF', async () => {
     const p = await $`node build/cli.js test/fixtures/markdown-crlf.md`
     assert.ok(p.stdout.includes('Hello, world!'))
test/export.test.js
@@ -75,17 +75,12 @@ describe('cli', () => {
     assert.equal(typeof cli.argv.v, 'boolean', 'cli.argv.v')
     assert.equal(typeof cli.argv.verbose, 'boolean', 'cli.argv.verbose')
     assert.equal(typeof cli.argv.version, 'boolean', 'cli.argv.version')
-    assert.equal(typeof cli.importPath, 'function', 'cli.importPath')
     assert.equal(typeof cli.injectGlobalRequire, 'function', 'cli.injectGlobalRequire')
     assert.equal(typeof cli.isMain, 'function', 'cli.isMain')
     assert.equal(typeof cli.main, 'function', 'cli.main')
     assert.equal(typeof cli.normalizeExt, 'function', 'cli.normalizeExt')
     assert.equal(typeof cli.printUsage, 'function', 'cli.printUsage')
-    assert.equal(typeof cli.runScript, 'function', 'cli.runScript')
-    assert.equal(typeof cli.scriptFromHttp, 'function', 'cli.scriptFromHttp')
-    assert.equal(typeof cli.scriptFromStdin, 'function', 'cli.scriptFromStdin')
     assert.equal(typeof cli.transformMarkdown, 'function', 'cli.transformMarkdown')
-    assert.equal(typeof cli.writeAndImport, 'function', 'cli.writeAndImport')
   })
 })