v7
  1// Copyright 2021 Google LLC
  2//
  3// Licensed under the Apache License, Version 2.0 (the "License");
  4// you may not use this file except in compliance with the License.
  5// You may obtain a copy of the License at
  6//
  7//     https://www.apache.org/licenses/LICENSE-2.0
  8//
  9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15import { $ } from './core.js'
 16import { spinner } from './experimental.js'
 17
 18export async function installDeps(
 19  dependencies: Record<string, string>,
 20  prefix?: string
 21) {
 22  const packages = Object.entries(dependencies).map(
 23    ([name, version]) => `${name}@${version}`
 24  )
 25  const flags = prefix ? `--prefix=${prefix}` : ''
 26  if (packages.length === 0) {
 27    return
 28  }
 29  await spinner(`npm i ${packages.join(' ')}`, () =>
 30    $`npm install --no-save --no-audit --no-fund ${flags} ${packages}`.nothrow()
 31  )
 32}
 33
 34const builtins = new Set([
 35  '_http_agent',
 36  '_http_client',
 37  '_http_common',
 38  '_http_incoming',
 39  '_http_outgoing',
 40  '_http_server',
 41  '_stream_duplex',
 42  '_stream_passthrough',
 43  '_stream_readable',
 44  '_stream_transform',
 45  '_stream_wrap',
 46  '_stream_writable',
 47  '_tls_common',
 48  '_tls_wrap',
 49  'assert',
 50  'async_hooks',
 51  'buffer',
 52  'child_process',
 53  'cluster',
 54  'console',
 55  'constants',
 56  'crypto',
 57  'dgram',
 58  'dns',
 59  'domain',
 60  'events',
 61  'fs',
 62  'http',
 63  'http2',
 64  'https',
 65  'inspector',
 66  'module',
 67  'net',
 68  'os',
 69  'path',
 70  'perf_hooks',
 71  'process',
 72  'punycode',
 73  'querystring',
 74  'readline',
 75  'repl',
 76  'stream',
 77  'string_decoder',
 78  'sys',
 79  'timers',
 80  'tls',
 81  'trace_events',
 82  'tty',
 83  'url',
 84  'util',
 85  'v8',
 86  'vm',
 87  'wasi',
 88  'worker_threads',
 89  'zlib',
 90])
 91const importRe = [
 92  /\bimport\s+['"](?<path>[^'"]+)['"]/,
 93  /\bimport\(['"](?<path>[^'"]+)['"]\)/,
 94  /\brequire\(['"](?<path>[^'"]+)['"]\)/,
 95  /\bfrom\s+['"](?<path>[^'"]+)['"]/,
 96]
 97const nameRe = /^(?<name>(@[a-z0-9-]+\/)?[a-z0-9-.]+)\/?.*$/i
 98const versionRe = /(\/\/|\/\*)\s*@(?<version>[~^]?([\dvx*]+([-.][\dx*]+)*))/i
 99
100export function parseDeps(content: Buffer): Record<string, string> {
101  const deps: Record<string, string> = {}
102  const lines = content.toString().split('\n')
103  for (let line of lines) {
104    const tuple = parseImports(line)
105    if (tuple) {
106      deps[tuple.name] = tuple.version
107    }
108  }
109  return deps
110}
111
112function parseImports(
113  line: string
114): { name: string; version: string } | undefined {
115  for (let re of importRe) {
116    const name = parsePackageName(re.exec(line)?.groups?.path)
117    const version = parseVersion(line)
118    if (name) {
119      return { name, version }
120    }
121  }
122}
123
124function parsePackageName(path?: string): string | undefined {
125  if (!path) return
126  const name = nameRe.exec(path)?.groups?.name
127  if (name && !builtins.has(name)) {
128    return name
129  }
130}
131
132function parseVersion(line: string) {
133  return versionRe.exec(line)?.groups?.version || 'latest'
134}