v7
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 minimist from 'minimist'
19import { createRequire } from 'node:module'
20import { basename, dirname, extname, join, resolve } from 'node:path'
21import url from 'node:url'
22import { updateArgv } from './goods.js'
23import { $, chalk, fetch, ProcessOutput } from './index.js'
24import { startRepl } from './repl.js'
25import { randomId } from './util.js'
26import { installDeps, parseDeps } from './deps.js'
27
28function printUsage() {
29 // language=txt
30 console.log(`
31 ${chalk.bold('zx ' + getVersion())}
32 A tool for writing better scripts
33
34 ${chalk.bold('Usage')}
35 zx [options] <script>
36
37 ${chalk.bold('Options')}
38 --quiet don't echo commands
39 --shell=<path> custom shell binary
40 --prefix=<command> prefix all commands
41 --eval=<js>, -e evaluate script
42 --install, -i install dependencies
43 --experimental enable experimental features
44 --version, -v print current zx version
45 --help, -h print help
46 --repl start repl
47`)
48}
49
50const argv = minimist(process.argv.slice(2), {
51 string: ['shell', 'prefix', 'eval'],
52 boolean: ['version', 'help', 'quiet', 'install', 'repl', 'experimental'],
53 alias: { e: 'eval', i: 'install', v: 'version', h: 'help' },
54 stopEarly: true,
55})
56
57await (async function main() {
58 const globals = './globals.js'
59 await import(globals)
60 if (argv.quiet) $.verbose = false
61 if (argv.shell) $.shell = argv.shell
62 if (argv.prefix) $.prefix = argv.prefix
63 if (argv.experimental) {
64 Object.assign(global, await import('./experimental.js'))
65 }
66 if (argv.version) {
67 console.log(getVersion())
68 return
69 }
70 if (argv.help) {
71 printUsage()
72 return
73 }
74 if (argv.repl) {
75 startRepl()
76 return
77 }
78 if (argv.eval) {
79 await runScript(argv.eval)
80 return
81 }
82 const firstArg = argv._[0]
83 updateArgv(argv._.slice(firstArg === undefined ? 0 : 1))
84 if (!firstArg || firstArg === '-') {
85 const success = await scriptFromStdin()
86 if (!success) printUsage()
87 return
88 }
89 if (/^https?:/.test(firstArg)) {
90 await scriptFromHttp(firstArg)
91 return
92 }
93 const filepath = firstArg.startsWith('file:///')
94 ? url.fileURLToPath(firstArg)
95 : resolve(firstArg)
96 await importPath(filepath)
97})().catch((err) => {
98 if (err instanceof ProcessOutput) {
99 console.error('Error:', err.message)
100 } else {
101 console.error(err)
102 }
103 process.exitCode = 1
104})
105
106async function runScript(script: string) {
107 const filepath = join(process.cwd(), `zx-${randomId()}.mjs`)
108 await writeAndImport(script, filepath)
109}
110
111async function scriptFromStdin() {
112 let script = ''
113 if (!process.stdin.isTTY) {
114 process.stdin.setEncoding('utf8')
115 for await (const chunk of process.stdin) {
116 script += chunk
117 }
118
119 if (script.length > 0) {
120 await runScript(script)
121 return true
122 }
123 }
124 return false
125}
126
127async function scriptFromHttp(remote: string) {
128 const res = await fetch(remote)
129 if (!res.ok) {
130 console.error(`Error: Can't get ${remote}`)
131 process.exit(1)
132 }
133 const script = await res.text()
134 const pathname = new URL(remote).pathname
135 const name = basename(pathname)
136 const ext = extname(pathname) || '.mjs'
137 const filepath = join(process.cwd(), `${name}-${randomId()}${ext}`)
138 await writeAndImport(script, filepath)
139}
140
141async function writeAndImport(
142 script: string | Buffer,
143 filepath: string,
144 origin = filepath
145) {
146 await fs.writeFile(filepath, script.toString())
147 try {
148 await importPath(filepath, origin)
149 } finally {
150 await fs.rm(filepath)
151 }
152}
153
154async function importPath(filepath: string, origin = filepath) {
155 const ext = extname(filepath)
156
157 if (ext === '') {
158 const tmpFilename = fs.existsSync(`${filepath}.mjs`)
159 ? `${basename(filepath)}-${randomId()}.mjs`
160 : `${basename(filepath)}.mjs`
161
162 return writeAndImport(
163 await fs.readFile(filepath),
164 join(dirname(filepath), tmpFilename),
165 origin
166 )
167 }
168 if (ext === '.md') {
169 return writeAndImport(
170 transformMarkdown(await fs.readFile(filepath)),
171 join(dirname(filepath), basename(filepath) + '.mjs'),
172 origin
173 )
174 }
175 if (argv.install) {
176 const deps = parseDeps(await fs.readFile(filepath))
177 await installDeps(deps, dirname(filepath))
178 }
179 const __filename = resolve(origin)
180 const __dirname = dirname(__filename)
181 const require = createRequire(origin)
182 Object.assign(global, { __filename, __dirname, require })
183 await import(url.pathToFileURL(filepath).toString())
184}
185
186function transformMarkdown(buf: Buffer) {
187 const source = buf.toString()
188 const output = []
189 let state = 'root'
190 let codeBlockEnd = ''
191 let prevLineIsEmpty = true
192 const jsCodeBlock = /^(```+|~~~+)(js|javascript)$/
193 const shCodeBlock = /^(```+|~~~+)(sh|bash)$/
194 const otherCodeBlock = /^(```+|~~~+)(.*)$/
195 for (let line of source.split('\n')) {
196 switch (state) {
197 case 'root':
198 if (/^( {4}|\t)/.test(line) && prevLineIsEmpty) {
199 output.push(line)
200 state = 'tab'
201 } else if (jsCodeBlock.test(line)) {
202 output.push('')
203 state = 'js'
204 codeBlockEnd = line.match(jsCodeBlock)![1]
205 } else if (shCodeBlock.test(line)) {
206 output.push('await $`')
207 state = 'bash'
208 codeBlockEnd = line.match(shCodeBlock)![1]
209 } else if (otherCodeBlock.test(line)) {
210 output.push('')
211 state = 'other'
212 codeBlockEnd = line.match(otherCodeBlock)![1]
213 } else {
214 prevLineIsEmpty = line === ''
215 output.push('// ' + line)
216 }
217 break
218 case 'tab':
219 if (/^( +|\t)/.test(line)) {
220 output.push(line)
221 } else if (line === '') {
222 output.push('')
223 } else {
224 output.push('// ' + line)
225 state = 'root'
226 }
227 break
228 case 'js':
229 if (line === codeBlockEnd) {
230 output.push('')
231 state = 'root'
232 } else {
233 output.push(line)
234 }
235 break
236 case 'bash':
237 if (line === codeBlockEnd) {
238 output.push('`')
239 state = 'root'
240 } else {
241 output.push(line)
242 }
243 break
244 case 'other':
245 if (line === codeBlockEnd) {
246 output.push('')
247 state = 'root'
248 } else {
249 output.push('// ' + line)
250 }
251 break
252 }
253 }
254 return output.join('\n')
255}
256
257function getVersion(): string {
258 return createRequire(import.meta.url)('../package.json').version
259}