+86
-92
@@ -16,71 +16,82 @@ #!/usr/bin/env node | ||
| import fs from 'fs-extra'; | ||
| import minimist from 'minimist'; | ||
| import { createRequire } from 'node:module'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { basename, dirname, extname, join, resolve } from 'node:path'; | ||
| import url from 'node:url'; | ||
| import { updateArgv } from './goods.js'; | ||
| import { $, argv, chalk, fetch, ProcessOutput } from './index.js'; | ||
| import { $, chalk, fetch, ProcessOutput } from './index.js'; | ||
| import { startRepl } from './repl.js'; | ||
| import { randomId } from './util.js'; | ||
| import { installDeps, parseDeps } from './deps.js'; | ||
| function printUsage() { | ||
| // language=txt | ||
| console.log(` | ||
| ${chalk.bold('zx ' + getVersion())} | ||
| A tool for writing better scripts | ||
| ${chalk.bold('Usage')} | ||
| zx [options] <script> | ||
| ${chalk.bold('Options')} | ||
| --quiet don't echo commands | ||
| --shell=<path> custom shell binary | ||
| --prefix=<command> prefix all commands | ||
| --eval=<js>, -e evaluate script | ||
| --install, -i install dependencies | ||
| --experimental enable experimental features | ||
| --version, -v print current zx version | ||
| --help, -h print help | ||
| --repl start repl | ||
| `); | ||
| } | ||
| const argv = minimist(process.argv.slice(2), { | ||
| string: ['shell', 'prefix', 'eval'], | ||
| boolean: ['version', 'help', 'quiet', 'install', 'repl', 'experimental'], | ||
| alias: { e: 'eval', i: 'install', v: 'version', h: 'help' }, | ||
| stopEarly: true, | ||
| }); | ||
| await (async function main() { | ||
| const globals = './globals.js'; | ||
| await import(globals); | ||
| if (argv.quiet) { | ||
| if (argv.quiet) | ||
| $.verbose = false; | ||
| } | ||
| if (typeof argv.shell === 'string') { | ||
| if (argv.shell) | ||
| $.shell = argv.shell; | ||
| } | ||
| if (typeof argv.prefix === 'string') { | ||
| if (argv.prefix) | ||
| $.prefix = argv.prefix; | ||
| } | ||
| if (argv.experimental) { | ||
| Object.assign(global, await import('./experimental.js')); | ||
| } | ||
| if (process.argv.length == 3) { | ||
| if (['--version', '-v', '-V'].includes(process.argv[2])) { | ||
| console.log(getVersion()); | ||
| return; | ||
| } | ||
| if (['--help', '-h'].includes(process.argv[2])) { | ||
| printUsage(); | ||
| return; | ||
| } | ||
| if (['--interactive', '-i'].includes(process.argv[2])) { | ||
| startRepl(); | ||
| return; | ||
| } | ||
| if (argv.version) { | ||
| console.log(getVersion()); | ||
| return; | ||
| } | ||
| if (argv.eval || argv.e) { | ||
| const script = (argv.eval || argv.e).toString(); | ||
| await runScript(script); | ||
| if (argv.help) { | ||
| printUsage(); | ||
| return; | ||
| } | ||
| let firstArg = process.argv.slice(2).find((a) => !a.startsWith('--')); | ||
| if (typeof firstArg === 'undefined' || firstArg === '-') { | ||
| let ok = await scriptFromStdin(); | ||
| if (!ok) { | ||
| startRepl(); | ||
| } | ||
| if (argv.repl) { | ||
| startRepl(); | ||
| return; | ||
| } | ||
| else if (firstArg.startsWith('http://') || | ||
| firstArg.startsWith('https://')) { | ||
| if (argv.eval) { | ||
| await runScript(argv.eval); | ||
| return; | ||
| } | ||
| const firstArg = argv._[0]; | ||
| updateArgv(argv._.slice(firstArg === undefined ? 0 : 1)); | ||
| if (!firstArg || firstArg === '-') { | ||
| const success = await scriptFromStdin(); | ||
| if (!success) | ||
| printUsage(); | ||
| return; | ||
| } | ||
| if (/^https?:/.test(firstArg)) { | ||
| await scriptFromHttp(firstArg); | ||
| return; | ||
| } | ||
| else { | ||
| let filepath; | ||
| if (firstArg.startsWith('/')) { | ||
| filepath = firstArg; | ||
| } | ||
| else if (firstArg.startsWith('file:///')) { | ||
| filepath = url.fileURLToPath(firstArg); | ||
| } | ||
| else { | ||
| filepath = resolve(firstArg); | ||
| } | ||
| updateArgv({ sliceAt: 3 }); | ||
| await importPath(filepath); | ||
| } | ||
| return; | ||
| const filepath = firstArg.startsWith('file:///') | ||
| ? url.fileURLToPath(firstArg) | ||
| : resolve(firstArg); | ||
| await importPath(filepath); | ||
| })().catch((err) => { | ||
@@ -96,5 +107,4 @@ if (err instanceof ProcessOutput) { | ||
| async function runScript(script) { | ||
| let filepath = join(tmpdir(), randomId() + '.mjs'); | ||
| await fs.mkdtemp(filepath); | ||
| await writeAndImport(script, filepath, join(process.cwd(), 'stdin.mjs')); | ||
| const filepath = join(process.cwd(), `zx-${randomId()}.mjs`); | ||
| await writeAndImport(script, filepath); | ||
| } | ||
@@ -116,3 +126,3 @@ async function scriptFromStdin() { | ||
| async function scriptFromHttp(remote) { | ||
| let res = await fetch(remote); | ||
| const res = await fetch(remote); | ||
| if (!res.ok) { | ||
@@ -122,32 +132,36 @@ console.error(`Error: Can't get ${remote}`); | ||
| } | ||
| let script = await res.text(); | ||
| let filename = new URL(remote).pathname; | ||
| let filepath = join(tmpdir(), basename(filename)); | ||
| await fs.mkdtemp(filepath); | ||
| await writeAndImport(script, filepath, join(process.cwd(), basename(filename))); | ||
| const script = await res.text(); | ||
| const pathname = new URL(remote).pathname; | ||
| const name = basename(pathname); | ||
| const ext = extname(pathname) || '.mjs'; | ||
| const filepath = join(process.cwd(), `${name}-${randomId()}${ext}`); | ||
| await writeAndImport(script, filepath); | ||
| } | ||
| async function writeAndImport(script, filepath, origin = filepath) { | ||
| const contents = script.toString(); | ||
| await fs.writeFile(filepath, contents); | ||
| if (argv.install) { | ||
| await installDeps(parseDeps(contents), dirname(filepath)); | ||
| await fs.writeFile(filepath, script.toString()); | ||
| try { | ||
| await importPath(filepath, origin); | ||
| } | ||
| let wait = importPath(filepath, origin); | ||
| await fs.rm(filepath); | ||
| await wait; | ||
| finally { | ||
| await fs.rm(filepath); | ||
| } | ||
| } | ||
| async function importPath(filepath, origin = filepath) { | ||
| let ext = extname(filepath); | ||
| const ext = extname(filepath); | ||
| if (ext === '') { | ||
| let tmpFilename = fs.existsSync(`${filepath}.mjs`) | ||
| const tmpFilename = fs.existsSync(`${filepath}.mjs`) | ||
| ? `${basename(filepath)}-${randomId()}.mjs` | ||
| : `${basename(filepath)}.mjs`; | ||
| return await writeAndImport(await fs.readFile(filepath), join(dirname(filepath), tmpFilename), origin); | ||
| return writeAndImport(await fs.readFile(filepath), join(dirname(filepath), tmpFilename), origin); | ||
| } | ||
| if (ext === '.md') { | ||
| return await writeAndImport(transformMarkdown(await fs.readFile(filepath)), join(dirname(filepath), basename(filepath) + '.mjs'), origin); | ||
| return writeAndImport(transformMarkdown(await fs.readFile(filepath)), join(dirname(filepath), basename(filepath) + '.mjs'), origin); | ||
| } | ||
| let __filename = resolve(origin); | ||
| let __dirname = dirname(__filename); | ||
| let require = createRequire(origin); | ||
| if (argv.install) { | ||
| const deps = parseDeps(await fs.readFile(filepath)); | ||
| await installDeps(deps, dirname(filepath)); | ||
| } | ||
| const __filename = resolve(origin); | ||
| const __dirname = dirname(__filename); | ||
| const require = createRequire(origin); | ||
| Object.assign(global, { __filename, __dirname, require }); | ||
@@ -157,4 +171,4 @@ await import(url.pathToFileURL(filepath).toString()); | ||
| function transformMarkdown(buf) { | ||
| let source = buf.toString(); | ||
| let output = []; | ||
| const source = buf.toString(); | ||
| const output = []; | ||
| let state = 'root'; | ||
@@ -232,21 +246,1 @@ let prevLineIsEmpty = true; | ||
| } | ||
| function printUsage() { | ||
| console.log(` | ||
| ${chalk.bold('zx ' + getVersion())} | ||
| A tool for writing better scripts | ||
| ${chalk.bold('Usage')} | ||
| zx [options] <script> | ||
| ${chalk.bold('Options')} | ||
| --quiet don't echo commands | ||
| --shell=<path> custom shell binary | ||
| --prefix=<command> prefix all commands | ||
| --interactive, -i start repl | ||
| --eval=<js>, -e evaluate script | ||
| --experimental enable new api proposals | ||
| --install parse and load script dependencies from the registry | ||
| --version, -v print current zx version | ||
| --help, -h print help | ||
| `); | ||
| } |
+1
-1
@@ -59,3 +59,3 @@ // Copyright 2021 Google LLC | ||
| let resolve, reject; | ||
| let promise = new ProcessPromise((...args) => ([resolve, reject] = args)); | ||
| const promise = new ProcessPromise((...args) => ([resolve, reject] = args)); | ||
| let cmd = pieces[0], i = 0; | ||
@@ -62,0 +62,0 @@ while (i < args.length) { |
+3
-2
@@ -1,2 +0,3 @@ | ||
| export declare function installDeps(dependencies?: Record<string, any>, prefix?: string): Promise<void>; | ||
| export declare function parseDeps(content: string): Record<string, any>; | ||
| /// <reference types="node" resolution-mode="require"/> | ||
| export declare function installDeps(dependencies: Record<string, string>, prefix?: string): Promise<void>; | ||
| export declare function parseDeps(content: Buffer): Record<string, string>; |
+96
-12
@@ -15,22 +15,106 @@ // Copyright 2021 Google LLC | ||
| import { $ } from './core.js'; | ||
| export async function installDeps(dependencies = {}, prefix) { | ||
| const pkgs = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`); | ||
| import { spinner } from './experimental.js'; | ||
| export async function installDeps(dependencies, prefix) { | ||
| const packages = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`); | ||
| const flags = prefix ? `--prefix=${prefix}` : ''; | ||
| if (pkgs.length === 0) { | ||
| if (packages.length === 0) { | ||
| return; | ||
| } | ||
| await $ `npm install --no-save --no-audit --no-fund ${flags} ${pkgs}`; | ||
| await spinner(`npm i ${packages.join(' ')}`, () => $ `npm install --no-save --no-audit --no-fund ${flags} ${packages}`.nothrow()); | ||
| } | ||
| const builtinsRe = /^(_http_agent|_http_client|_http_common|_http_incoming|_http_outgoing|_http_server|_stream_duplex|_stream_passthrough|_stream_readable|_stream_transform|_stream_wrap|_stream_writable|_tls_common|_tls_wrap|assert|async_hooks|buffer|child_process|cluster|console|constants|crypto|dgram|diagnostics_channel|dns|domain|events|fs|http|http2|https|inspector|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|trace_events|tty|url|util|v8|vm|wasi|worker_threads|zlib)$/; | ||
| const builtins = new Set([ | ||
| '_http_agent', | ||
| '_http_client', | ||
| '_http_common', | ||
| '_http_incoming', | ||
| '_http_outgoing', | ||
| '_http_server', | ||
| '_stream_duplex', | ||
| '_stream_passthrough', | ||
| '_stream_readable', | ||
| '_stream_transform', | ||
| '_stream_wrap', | ||
| '_stream_writable', | ||
| '_tls_common', | ||
| '_tls_wrap', | ||
| 'assert', | ||
| 'async_hooks', | ||
| 'buffer', | ||
| 'child_process', | ||
| 'cluster', | ||
| 'console', | ||
| 'constants', | ||
| 'crypto', | ||
| 'dgram', | ||
| 'dns', | ||
| 'domain', | ||
| 'events', | ||
| 'fs', | ||
| 'http', | ||
| 'http2', | ||
| 'https', | ||
| 'inspector', | ||
| 'module', | ||
| 'net', | ||
| 'os', | ||
| 'path', | ||
| 'perf_hooks', | ||
| 'process', | ||
| 'punycode', | ||
| 'querystring', | ||
| 'readline', | ||
| 'repl', | ||
| 'stream', | ||
| 'string_decoder', | ||
| 'sys', | ||
| 'timers', | ||
| 'tls', | ||
| 'trace_events', | ||
| 'tty', | ||
| 'url', | ||
| 'util', | ||
| 'v8', | ||
| 'vm', | ||
| 'wasi', | ||
| 'worker_threads', | ||
| 'zlib', | ||
| ]); | ||
| const importRe = [ | ||
| /\bimport\s+['"](?<path>[^'"]+)['"]/, | ||
| /\bimport\(['"](?<path>[^'"]+)['"]\)/, | ||
| /\brequire\(['"](?<path>[^'"]+)['"]\)/, | ||
| /\bfrom\s+['"](?<path>[^'"]+)['"]/, | ||
| ]; | ||
| const nameRe = /^(?<name>(@[a-z0-9-]+\/)?[a-z0-9-]+)\/?.*$/i; | ||
| const versionRe = /(\/\/|\/\*)\s*@(?<version>[~^]?([\dvx*]+([-.][\dx*]+)*))/i; | ||
| export function parseDeps(content) { | ||
| const re = /(?:\sfrom\s+|[\s(:\[](?:import|require)\s*\()["']((?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*)[/a-z0-9-._~]*["'](?:\s*;?\s*(?:\/\*|\/\/)\s*([a-z0-9-._~^*]+))?/g; | ||
| const deps = {}; | ||
| let m; | ||
| do { | ||
| m = re.exec(content); | ||
| if (m && !builtinsRe.test(m[1])) { | ||
| deps[m[1]] = m[2] || 'latest'; | ||
| const lines = content.toString().split('\n'); | ||
| for (let line of lines) { | ||
| const tuple = parseImports(line); | ||
| if (tuple) { | ||
| deps[tuple.name] = tuple.version; | ||
| } | ||
| } while (m); | ||
| } | ||
| return deps; | ||
| } | ||
| function parseImports(line) { | ||
| for (let re of importRe) { | ||
| const name = parsePackageName(re.exec(line)?.groups?.path); | ||
| const version = parseVersion(line); | ||
| if (name) { | ||
| return { name, version }; | ||
| } | ||
| } | ||
| } | ||
| function parsePackageName(path) { | ||
| if (!path) | ||
| return; | ||
| const name = nameRe.exec(path)?.groups?.name; | ||
| if (name && !builtins.has(name)) { | ||
| return name; | ||
| } | ||
| } | ||
| function parseVersion(line) { | ||
| return versionRe.exec(line)?.groups?.version || 'latest'; | ||
| } |
+1
-3
@@ -12,5 +12,3 @@ import * as globbyModule from 'globby'; | ||
| export declare let argv: minimist.ParsedArgs; | ||
| export declare function updateArgv(params: { | ||
| sliceAt: number; | ||
| }): void; | ||
| export declare function updateArgv(args: string[]): void; | ||
| export declare const globby: ((patterns: string | readonly string[], options?: globbyModule.Options) => Promise<string[]>) & typeof globbyModule; | ||
@@ -17,0 +15,0 @@ export declare const glob: ((patterns: string | readonly string[], options?: globbyModule.Options) => Promise<string[]>) & typeof globbyModule; |
+3
-3
@@ -27,4 +27,4 @@ // Copyright 2022 Google LLC | ||
| export let argv = minimist(process.argv.slice(2)); | ||
| export function updateArgv(params) { | ||
| argv = minimist(process.argv.slice(params.sliceAt)); | ||
| export function updateArgv(args) { | ||
| argv = minimist(args); | ||
| global.argv = argv; | ||
@@ -47,3 +47,3 @@ } | ||
| let msg; | ||
| let lastIdx = pieces.length - 1; | ||
| const lastIdx = pieces.length - 1; | ||
| if (Array.isArray(pieces) && | ||
@@ -50,0 +50,0 @@ pieces.every(isString) && |
+2
-2
| { | ||
| "name": "zx", | ||
| "version": "7.1.0-dev.2dbf8e8", | ||
| "version": "7.1.0-dev.38c4497", | ||
| "description": "A tool for writing better scripts.", | ||
@@ -72,3 +72,3 @@ "type": "module", | ||
| "prettier": "^2.7.1", | ||
| "tsd": "^0.24.0", | ||
| "tsd": "^0.24.1", | ||
| "typescript": "^4.8.3", | ||
@@ -75,0 +75,0 @@ "uvu": "^0.5.6" |
+47
-24
@@ -353,4 +353,19 @@ # 🐚 zx | ||
| Specifies a [logging function](src/log.ts). | ||
| Specifies a [logging function](src/core.ts). | ||
| ```ts | ||
| import { LogEntry, log } from 'zx/core' | ||
| $.log = (entry: LogEntry) => { | ||
| switch (entry.kind) { | ||
| case 'cmd': | ||
| // for example, apply custom data masker for cmd printing | ||
| process.stderr.write(masker(entry.cmd)) | ||
| break | ||
| default: | ||
| log(entry) | ||
| } | ||
| } | ||
| ``` | ||
| ## Polyfills | ||
@@ -411,24 +426,2 @@ | ||
| ## CLI | ||
| | Flag | Description | Default | | ||
| |-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | ||
| | `--quiet` | don't echo commands | `false` | | ||
| | `--shell=<path>` | custom shell binary | | | ||
| | `--prefix=<command>` | prefix all commands | | | ||
| | `--interactive, -i` | start repl | | | ||
| | `--eval=<js>, -e` | evaluate script | | | ||
| | `--experimental` | enable new api proposals | | | ||
| | `--install` | parse and load script dependencies from the registry. You can pass additional [params via env vars](https://docs.npmjs.com/cli/v8/using-npm/config) like `npm_config_registry=<url>` or `npm_config_userconfig=<path>`. | `false` | | ||
| | `--version, -v` | print current zx version | | | ||
| | `--help, -h` | print help | | | ||
| ```bash | ||
| zx script.js | ||
| zx --help | ||
| zx --experimental <<'EOF' | ||
| await $`pwd` | ||
| EOF | ||
| ``` | ||
| ## FAQ | ||
@@ -521,6 +514,29 @@ | ||
| ### Installing dependencies via --install | ||
| ```js | ||
| // script.mjs: | ||
| import sh from 'tinysh' | ||
| sh.say('Hello, world!') | ||
| ``` | ||
| Add `--install` flag to the `zx` command to install missing dependencies | ||
| automatically. | ||
| ```bash | ||
| zx --install script.mjs | ||
| ``` | ||
| You can also specify needed version by adding comment with `@` after | ||
| the import. | ||
| ```js | ||
| import sh from 'tinysh' // @^1 | ||
| ``` | ||
| ### Attaching a profile | ||
| By default `child_process` does not include aliases and bash functions. | ||
| But you are still able to do it by hand. Just attach necessary directives to `$.prefix`. | ||
| But you are still able to do it by hand. Just attach necessary directives | ||
| to the `$.prefix`. | ||
@@ -552,2 +568,9 @@ ```js | ||
| ### Canary / Beta / RC builds | ||
| Impatient early adopters can try the experimental zx versions. But keep in mind: these builds are ⚠️️ __unstable__ in every sense. | ||
| ```bash | ||
| npm i zx@dev | ||
| npx zx@dev --install --quiet <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)' | ||
| ``` | ||
| ## License | ||
@@ -554,0 +577,0 @@ |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
1502
5.26%577
4.15%75012
-1.4%