Commit 7be867d
Changed files (7)
examples/basics.mjs
@@ -24,5 +24,5 @@ await $`echo ${foo} | wc` // Vars properly quoted.
// We can use import and require together.
let path = await import('path')
-let {version} = require(path.join(__dirname, '..', 'package.json'))
-console.log(chalk.black.bgYellowBright(version))
+let {name} = require(path.join(__dirname, '..', 'package.json'))
+console.log(chalk.black.bgCyanBright(name))
examples/index.md
@@ -0,0 +1,17 @@
+# Markdown Scripts
+
+It's possible to write scripts using markdown. Only code blocks will be executed
+by zx. Try to run `zx examples/index.md`.
+
+```js
+await $`whoami`
+await $`ls -la ${__dirname}`
+```
+
+The `__filename` will be pointed to **index.md**:
+
+ console.log(chalk.yellowBright(__filename))
+
+We can use imports here as well:
+
+ await import('./basics.mjs')
examples/no-extension
@@ -0,0 +1,20 @@
+#!/usr/bin/env zx
+
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+console.log(chalk.yellowBright`If file has no extension, zx assumes it's ESM.`)
+await $`pwd`
+console.log('__filename =', __filename)
+console.log('__dirname =', __dirname)
index.mjs
@@ -24,7 +24,7 @@ import shq from 'shq'
export {chalk}
function colorize(cmd) {
- return cmd.replace(/^\w+\s/, substr => {
+ return cmd.replace(/^\w+(\s|$)/, substr => {
return chalk.greenBright(substr)
})
}
README.md
@@ -225,16 +225,6 @@ files (when using `zx` executable).
let {version} = require('./package.json')
```
-### Importing from other scripts
-
-It is possible to make use of `$` and other functions via explicit imports:
-
-```js
-#!/usr/bin/env node
-import {$} from 'zx'
-await $`date`
-```
-
### Passing env variables
```js
@@ -253,9 +243,34 @@ let files = [...]
await $`tar cz ${files}`
```
+### Importing from other scripts
+
+It is possible to make use of `$` and other functions via explicit imports:
+
+```js
+#!/usr/bin/env node
+import {$} from 'zx'
+await $`date`
+```
+
+### Scripts without extensions
+
+If script does not have a file extension (like `.git/hooks/pre-commit`), zx
+assumes what it is a [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename)
+module.
+
+### Markdown scripts
+
+The `zx` can execute scripts written in markdown
+([examples/index.md](examples/index.md)):
+
+```bash
+zx examples/index.md
+```
+
### Executing remote scripts
-If the argument to the `zx` executable starts with `https://`, the file will be
+If the argument to the `zx` executable starts with `https://`, the file will be
downloaded and executed.
```bash
test.mjs
@@ -75,6 +75,18 @@ import {strict as assert} from 'assert'
}
}
+{ // Scripts with no extension are working
+ await $`node zx.mjs examples/no-extension`
+}
+
+{ // require() is working from stdin
+ await $`node zx.mjs <<< 'require("./package.json").name'`
+}
+
+{ // Markdown scripts are working
+ await $`node zx.mjs examples/index.md`
+}
+
{ // require() is working in ESM
const {name, version} = require('./package.json')
assert(typeof name === 'string')
zx.mjs
@@ -14,7 +14,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {join, basename, resolve, dirname} from 'path'
+import {join, basename, extname, resolve, dirname} from 'path'
import os, {tmpdir} from 'os'
import {promises as fs} from 'fs'
import {createRequire} from 'module'
@@ -49,13 +49,15 @@ try {
} else if (firstArg.startsWith('http://') || firstArg.startsWith('https://')) {
await scriptFromHttp(firstArg)
} else {
- let path
- if (firstArg.startsWith('/') || firstArg.startsWith('file:///')) {
- path = firstArg
+ let filepath
+ if (firstArg.startsWith('/')) {
+ filepath = firstArg
+ } else if (firstArg.startsWith('file:///')) {
+ filepath = url.fileURLToPath(firstArg)
} else {
- path = join(process.cwd(), firstArg)
+ filepath = join(process.cwd(), firstArg)
}
- await importPath(path)
+ await importPath(filepath)
}
} catch (p) {
@@ -76,43 +78,97 @@ async function scriptFromStdin() {
}
if (script.length > 0) {
- let filepath = join(tmpdir(), randomId() + '.mjs')
- await writeAndImport(filepath, script)
+ let filepath = join(
+ tmpdir(),
+ Math.random().toString(36).substr(2) + '.mjs'
+ )
+ await fs.mkdtemp(filepath)
+ await writeAndImport(script, filepath, join(process.cwd(), 'stdin.mjs'))
return true
}
}
return false
}
-async function scriptFromHttp(firstArg) {
- let res = await fetch(firstArg)
+async function scriptFromHttp(remote) {
+ let res = await fetch(remote)
if (!res.ok) {
- console.error(`Error: Can't get ${firstArg}`)
+ console.error(`Error: Can't get ${remote}`)
process.exit(1)
}
let script = await res.text()
- let filepath = join(tmpdir(), basename(firstArg))
- await writeAndImport(filepath, script)
+ let filepath = join(tmpdir(), basename(remote))
+ await fs.mkdtemp(filepath)
+ await writeAndImport(script, filepath, join(process.cwd(), basename(remote)))
}
-async function writeAndImport(filepath, script) {
- await fs.mkdtemp(filepath)
- try {
- await fs.writeFile(filepath, script)
- await import(url.pathToFileURL(filepath))
- } finally {
- await fs.rm(filepath)
- }
+async function writeAndImport(script, filepath, origin = filepath) {
+ await fs.writeFile(filepath, script)
+ let wait = importPath(filepath, origin)
+ await fs.rm(filepath)
+ await wait
}
-async function importPath(filepath) {
- let __filename = resolve(filepath)
+async function importPath(filepath, origin = filepath) {
+ let ext = extname(filepath)
+ if (ext === '') {
+ return await writeAndImport(
+ await fs.readFile(filepath),
+ join(dirname(filepath), basename(filepath) + '.mjs'),
+ origin,
+ )
+ }
+ if (ext === '.md') {
+ return await writeAndImport(
+ transformMarkdown((await fs.readFile(filepath)).toString()),
+ join(dirname(filepath), basename(filepath) + '.mjs'),
+ origin,
+ )
+ }
+ let __filename = resolve(origin)
let __dirname = dirname(__filename)
- let require = createRequire(filepath)
+ let require = createRequire(origin)
Object.assign(global, {__filename, __dirname, require})
await import(url.pathToFileURL(filepath))
}
-function randomId() {
- return Math.random().toString(36).substr(2)
+function transformMarkdown(source) {
+ let output = []
+ let state = 'root'
+ let prevLineIsEmpty = true
+ for (let line of source.split('\n')) {
+ switch (state) {
+ case 'root':
+ if (/^( {4}|\t)/.test(line) && prevLineIsEmpty) {
+ output.push(line)
+ state = 'tab'
+ } else if (/^```(js)?$/.test(line)) {
+ output.push('')
+ state = 'code'
+ } else {
+ prevLineIsEmpty = line === ''
+ output.push('// ' + line)
+ }
+ break
+ case 'tab':
+ if (/^( +|\t)/.test(line)) {
+ output.push(line)
+ } else if (line === '') {
+ output.push('')
+ } else {
+ output.push('// ' + line)
+ state = 'root'
+ }
+ break
+ case 'code':
+ if (/^```$/.test(line)) {
+ output.push('')
+ state = 'root'
+ } else {
+ output.push(line)
+ }
+ break
+ }
+ }
+ return output.join('\n')
}