v6
  1#!/usr/bin/env node
  2
  3// Copyright 2021 Google LLC
  4//
  5// Licensed under the Apache License, Version 2.0 (the "License");
  6// you may not use this file except in compliance with the License.
  7// You may obtain a copy of the License at
  8//
  9//     https://www.apache.org/licenses/LICENSE-2.0
 10//
 11// Unless required by applicable law or agreed to in writing, software
 12// distributed under the License is distributed on an "AS IS" BASIS,
 13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14// See the License for the specific language governing permissions and
 15// limitations under the License.
 16
 17import fs from 'fs-extra'
 18import { createRequire } from 'node:module'
 19import { tmpdir } from 'node:os'
 20import { basename, dirname, extname, join, resolve } from 'node:path'
 21import url from 'node:url'
 22
 23import { $, argv, fetch, ProcessOutput, chalk } from './index.js'
 24import { randomId } from './util.js'
 25import './globals.js'
 26
 27await (async function main() {
 28  $.verbose = +(argv.verbose || argv.quiet ? 0 : $.verbose)
 29
 30  if (typeof argv.shell === 'string') {
 31    $.shell = argv.shell
 32  }
 33  if (typeof argv.prefix === 'string') {
 34    $.prefix = argv.prefix
 35  }
 36  if (argv.experimental) {
 37    Object.assign(global, await import('./experimental.js'))
 38  }
 39  try {
 40    if (['--version', '-v', '-V'].includes(process.argv[2])) {
 41      console.log(createRequire(import.meta.url)('../package.json').version)
 42      return (process.exitCode = 0)
 43    }
 44    let firstArg = process.argv.slice(2).find((a) => !a.startsWith('--'))
 45    if (typeof firstArg === 'undefined' || firstArg === '-') {
 46      let ok = await scriptFromStdin()
 47      if (!ok) {
 48        printUsage()
 49        return (process.exitCode = 2)
 50      }
 51    } else if (
 52      firstArg.startsWith('http://') ||
 53      firstArg.startsWith('https://')
 54    ) {
 55      await scriptFromHttp(firstArg)
 56    } else {
 57      let filepath
 58      if (firstArg.startsWith('/')) {
 59        filepath = firstArg
 60      } else if (firstArg.startsWith('file:///')) {
 61        filepath = url.fileURLToPath(firstArg)
 62      } else {
 63        filepath = resolve(firstArg)
 64      }
 65      await importPath(filepath)
 66    }
 67  } catch (p) {
 68    if (p instanceof ProcessOutput) {
 69      console.error('Error: ' + p.message)
 70      return (process.exitCode = 1)
 71    } else {
 72      throw p
 73    }
 74  }
 75  return (process.exitCode = 0)
 76})()
 77
 78async function scriptFromStdin() {
 79  let script = ''
 80  if (!process.stdin.isTTY) {
 81    process.stdin.setEncoding('utf8')
 82    for await (const chunk of process.stdin) {
 83      script += chunk
 84    }
 85
 86    if (script.length > 0) {
 87      let filepath = join(tmpdir(), randomId() + '.mjs')
 88      await fs.mkdtemp(filepath)
 89      await writeAndImport(script, filepath, join(process.cwd(), 'stdin.mjs'))
 90      return true
 91    }
 92  }
 93  return false
 94}
 95
 96async function scriptFromHttp(remote: string) {
 97  let res = await fetch(remote)
 98  if (!res.ok) {
 99    console.error(`Error: Can't get ${remote}`)
100    process.exit(1)
101  }
102  let script = await res.text()
103  let filename = new URL(remote).pathname
104  let filepath = join(tmpdir(), basename(filename))
105  await fs.mkdtemp(filepath)
106  await writeAndImport(
107    script,
108    filepath,
109    join(process.cwd(), basename(filename))
110  )
111}
112
113async function writeAndImport(
114  script: string | Buffer,
115  filepath: string,
116  origin = filepath
117) {
118  await fs.writeFile(filepath, script.toString())
119  let wait = importPath(filepath, origin)
120  await fs.rm(filepath)
121  await wait
122}
123
124async function importPath(filepath: string, origin = filepath) {
125  let ext = extname(filepath)
126
127  if (ext === '') {
128    let tmpFilename = fs.existsSync(`${filepath}.mjs`)
129      ? `${basename(filepath)}-${randomId()}.mjs`
130      : `${basename(filepath)}.mjs`
131
132    return await writeAndImport(
133      await fs.readFile(filepath),
134      join(dirname(filepath), tmpFilename),
135      origin
136    )
137  }
138  if (ext === '.md') {
139    return await writeAndImport(
140      transformMarkdown(await fs.readFile(filepath)),
141      join(dirname(filepath), basename(filepath) + '.mjs'),
142      origin
143    )
144  }
145  let __filename = resolve(origin)
146  let __dirname = dirname(__filename)
147  let require = createRequire(origin)
148  Object.assign(global, { __filename, __dirname, require })
149  await import(url.pathToFileURL(filepath).toString())
150}
151
152function transformMarkdown(buf: Buffer) {
153  let source = buf.toString()
154  let output = []
155  let state = 'root'
156  let prevLineIsEmpty = true
157  for (let line of source.split('\n')) {
158    switch (state) {
159      case 'root':
160        if (/^( {4}|\t)/.test(line) && prevLineIsEmpty) {
161          output.push(line)
162          state = 'tab'
163        } else if (/^```(js|javascript)$/.test(line)) {
164          output.push('')
165          state = 'js'
166        } else if (/^```(sh|bash)$/.test(line)) {
167          output.push('await $`')
168          state = 'bash'
169        } else if (/^```.*$/.test(line)) {
170          output.push('')
171          state = 'other'
172        } else {
173          prevLineIsEmpty = line === ''
174          output.push('// ' + line)
175        }
176        break
177      case 'tab':
178        if (/^( +|\t)/.test(line)) {
179          output.push(line)
180        } else if (line === '') {
181          output.push('')
182        } else {
183          output.push('// ' + line)
184          state = 'root'
185        }
186        break
187      case 'js':
188        if (/^```$/.test(line)) {
189          output.push('')
190          state = 'root'
191        } else {
192          output.push(line)
193        }
194        break
195      case 'bash':
196        if (/^```$/.test(line)) {
197          output.push('`')
198          state = 'root'
199        } else {
200          output.push(line)
201        }
202        break
203      case 'other':
204        if (/^```$/.test(line)) {
205          output.push('')
206          state = 'root'
207        } else {
208          output.push('// ' + line)
209        }
210        break
211    }
212  }
213  return output.join('\n')
214}
215
216function printUsage() {
217  console.log(`
218 ${chalk.bgGreenBright.black(' ZX ')}
219
220 Usage:
221   zx [options] <script>
222
223 Options:
224   --verbose          : verbosity level 0|1|2
225   --quiet            : don't echo commands, same as --verbose=0
226   --shell=<path>     : custom shell binary
227   --prefix=<command> : prefix all commands
228   --experimental     : enable new api proposals
229   --version, -v      : print current zx version
230`)
231}