From 75a9549014d7515e1a647c862507c8919a99180c Mon Sep 17 00:00:00 2001
From: Anton Golub <antongolub@antongolub.com>
Date: Sat, 11 Jan 2025 17:44:12 +0300
Subject: [PATCH 1/2] feat(cli): process md for any kind of input

closes #1076
---
 src/cli.ts            | 147 ++++++++++++++++++++----------------------
 test/cli.test.js      |  16 +++--
 test/export.test.js   |   5 --
 test/fixtures/md.http |   9 +++
 4 files changed, 91 insertions(+), 86 deletions(-)
 create mode 100644 test/fixtures/md.http

diff --git a/src/cli.ts b/src/cli.ts
index 94165e1a09..c6b6b7a062 100644
--- a/src/cli.ts
+++ b/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,94 @@ 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._
+  updateArgv(argv._.slice(firstArg === undefined ? 0 : 1))
+
+  let script = ''
+  let scriptPath = ''
+  let tempPath = ''
 
+  if (argv.eval) {
+    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)) {
+    script = await readScriptFromHttp(firstArg)
+    const { name, ext = argv.ext } = path.parse(new URL(firstArg).pathname)
+    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)
   }
 
-  injectGlobalRequire(origin)
-  // TODO: fix unanalyzable-dynamic-import to work correctly with jsr.io
-  await import(url.pathToFileURL(filepath).toString())
+  return { script, scriptPath, tempPath }
+}
+
+async function readScriptFromStdin(): Promise<string> {
+  return !process.stdin.isTTY ? stdin() : ''
+}
+
+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) {
diff --git a/test/cli.test.js b/test/cli.test.js
index a63275d007..8a095ec7e2 100644
--- a/test/cli.test.js
+++ b/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!'))
diff --git a/test/export.test.js b/test/export.test.js
index 83ef319ca0..6e07584e51 100644
--- a/test/export.test.js
+++ b/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')
   })
 })
 
diff --git a/test/fixtures/md.http b/test/fixtures/md.http
new file mode 100644
index 0000000000..c6f67d7742
--- /dev/null
+++ b/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'`
+```

From 2271e97d3c5bb4a69aee698260ae43b0be5e94de Mon Sep 17 00:00:00 2001
From: Anton Golub <antongolub@antongolub.com>
Date: Sat, 11 Jan 2025 19:39:56 +0300
Subject: [PATCH 2/2] fix(cli): fix updateArgv block

---
 src/cli.ts | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/cli.ts b/src/cli.ts
index c6b6b7a062..6b77ea6568 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -151,13 +151,13 @@ async function runScript(
 
 async function readScript() {
   const [firstArg] = argv._
-  updateArgv(argv._.slice(firstArg === undefined ? 0 : 1))
-
   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 === '-') {
@@ -169,8 +169,8 @@ async function readScript() {
       throw new Error('No script provided')
     }
   } else if (/^https?:/.test(firstArg)) {
-    script = await readScriptFromHttp(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')
@@ -187,6 +187,7 @@ async function readScript() {
     script = transformMarkdown(script)
     tempPath = getFilepath(dir, base)
   }
+  if (argSlice) updateArgv(argv._.slice(argSlice))
 
   return { script, scriptPath, tempPath }
 }