Commit 088bbec

Anton Golub <antongolub@antongolub.com>
2022-08-28 12:11:08
feat: provide API to load modules from npm registry (#498)
1 parent 53fd6db
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