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}