Commit 088bbec
Changed files (9)
docs/quotes.md
@@ -73,5 +73,3 @@ Use `glob` function and `os` package:
let files = await glob(os.homedir() + '/dev/**/*.md')
await $`ls ${files}`
```
-
-
src/cli.ts
@@ -23,6 +23,7 @@ import { updateArgv } from './goods.js'
import { $, argv, chalk, fetch, ProcessOutput } from './index.js'
import { startRepl } from './repl.js'
import { randomId } from './util.js'
+import { installDeps, parseDeps } from './deps.js'
await (async function main() {
const globals = './globals.js'
@@ -135,7 +136,13 @@ async function writeAndImport(
filepath: string,
origin = filepath
) {
- await fs.writeFile(filepath, script.toString())
+ const contents = script.toString()
+ await fs.writeFile(filepath, contents)
+
+ if (argv.install) {
+ await installDeps(parseDeps(contents), dirname(filepath))
+ }
+
let wait = importPath(filepath, origin)
await fs.rm(filepath)
await wait
@@ -246,13 +253,14 @@ function printUsage() {
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
- --version, -v print current zx version
- --help, -h print help
+ --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
`)
}
src/deps.ts
@@ -0,0 +1,51 @@
+// 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.
+
+import { $ } from './core.js'
+
+export async function installDeps(
+ dependencies: Record<string, any> = {},
+ prefix?: string
+) {
+ const pkgs = Object.entries(dependencies).map(
+ ([name, version]) => `${name}@${version}`
+ )
+
+ const flags = prefix ? `--prefix=${prefix}` : ''
+
+ if (pkgs.length === 0) {
+ return
+ }
+
+ await $`npm install --no-save --no-audit --no-fund ${flags} ${pkgs}`
+}
+
+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)$/
+
+export function parseDeps(content: string): Record<string, any> {
+ 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: Record<string, any> = {}
+ let m
+
+ do {
+ m = re.exec(content)
+ if (m && !builtinsRe.test(m[1])) {
+ deps[m[1]] = m[2] || 'latest'
+ }
+ } while (m)
+
+ return deps
+}
src/util.ts
@@ -29,7 +29,7 @@ export function isString(obj: any) {
}
export function quote(arg: string) {
- if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {
+ if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
return arg
}
return (
test/deps.test.js
@@ -0,0 +1,60 @@
+// 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.
+
+import { suite } from 'uvu'
+import * as assert from 'uvu/assert'
+import { $ } from '../build/index.js'
+import { installDeps, parseDeps } from '../build/deps.js'
+
+const test = suite('deps')
+
+$.verbose = false
+
+test('installDeps() loader works via JS API', async () => {
+ await installDeps({
+ cpy: '9.0.1',
+ 'lodash-es': '4.17.21',
+ })
+ assert.instance((await import('cpy')).default, Function)
+ assert.instance((await import('lodash-es')).pick, Function)
+})
+
+test('installDeps() loader works via CLI', async () => {
+ let out =
+ await $`npm_config_registry="https://registry.yarnpkg.com" node build/cli.js --install <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)'`
+ assert.match(out.stdout, '4.17.15')
+})
+
+test('parseDeps() extracts deps map', () => {
+ const contents = `
+ import fs from 'fs'
+ import path from 'path'
+ import foo from "foo"
+ import bar from "bar" /* 1.0.0 */
+ import baz from "baz" // ^2.0
+
+ const cpy = await import('cpy')
+ const { pick } = require('lodash')
+ `
+
+ assert.equal(parseDeps(contents), {
+ foo: 'latest',
+ bar: '1.0.0',
+ baz: '^2.0',
+ cpy: 'latest',
+ lodash: 'latest',
+ })
+})
+
+test.run()
test/goods.test.js
@@ -78,7 +78,7 @@ test('which() available', async () => {
test('sleep() works', async () => {
const now = Date.now()
await sleep(100)
- assert.ok(Date.now() >= now + 100)
+ assert.ok(Date.now() >= now + 99)
})
test.run()
package-lock.json
@@ -1,12 +1,12 @@
{
"name": "zx",
- "version": "7.0.7",
+ "version": "7.0.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "zx",
- "version": "7.0.7",
+ "version": "7.0.8",
"license": "Apache-2.0",
"dependencies": {
"@types/fs-extra": "^9.0.13",
README.md
@@ -407,6 +407,28 @@ await spinner(() => $`long-running command`)
await spinner('working...', () => $`sleep 99`)
```
+## 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
### Passing env variables
tsconfig.json
@@ -2,8 +2,8 @@
"compilerOptions": {
"target": "ES2021",
"lib": ["ES2021"],
- "moduleResolution": "nodenext",
- "module": "nodenext",
+ "moduleResolution": "NodeNext",
+ "module": "NodeNext",
"strict": true,
"outDir": "./build",
"declaration": true