main
 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 { builtinModules } from 'node:module'
16import { $, spinner, Fail } from './index.ts'
17import { depseek } from './vendor.ts'
18
19/**
20 * Install npm dependencies
21 * @param dependencies object of dependencies
22 * @param prefix  path to the directory where npm should install the dependencies
23 * @param registry custom npm registry URL when installing dependencies
24 * @param installerType package manager: npm, yarn, pnpm, bun, etc.
25 */
26export async function installDeps(
27  dependencies: Record<string, string>,
28  prefix?: string,
29  registry?: string,
30  installerType = 'npm'
31): Promise<void> {
32  const installer = installers[installerType]
33  const packages = Object.entries(dependencies).map(
34    ([name, version]) => `${name}@${version}`
35  )
36  if (packages.length === 0) return
37  if (!installer) {
38    throw new Fail(
39      `Unsupported installer type: ${installerType}. Supported types: ${Object.keys(installers).join(', ')}`
40    )
41  }
42
43  await spinner(`${installerType} i ${packages.join(' ')}`, () =>
44    installer({ packages, prefix, registry })
45  )
46}
47
48type DepsInstaller = (opts: {
49  packages: string[]
50  registry?: string
51  prefix?: string
52}) => Promise<void>
53
54const installers: Record<any, DepsInstaller> = {
55  npm: async ({ packages, prefix, registry }) => {
56    const flags = [
57      '--no-save',
58      '--no-audit',
59      '--no-fund',
60      prefix && `--prefix=${prefix}`,
61      registry && `--registry=${registry}`,
62    ].filter(Boolean)
63    await $`npm install ${flags} ${packages}`.nothrow()
64  },
65}
66
67const builtins = new Set(builtinModules)
68
69const nameRe = /^(?<name>(@[a-z\d-~][\w-.~]*\/)?[a-z\d-~][\w-.~]*)\/?.*$/i
70const versionRe = /^@(?<version>[~^]?(v?[\dx*]+([-.][\d*a-z-]+)*))/i
71
72export function parseDeps(content: string): Record<string, string> {
73  return depseek(content + '\n', { comments: true }).reduce<
74    Record<string, string>
75  >((m, { type, value }, i, list) => {
76    if (type === 'dep') {
77      const meta = list[i + 1]
78      const name = parsePackageName(value)
79      const version =
80        (meta?.type === 'comment' && parseVersion(meta?.value.trim())) ||
81        'latest'
82      if (name) m[name] = version
83    }
84    return m
85  }, {})
86}
87
88function parsePackageName(path?: string): string | undefined {
89  if (!path) return
90
91  const name = nameRe.exec(path)?.groups?.name
92  if (name && !builtins.has(name)) return name
93}
94
95function parseVersion(line: string) {
96  return versionRe.exec(line)?.groups?.version || 'latest'
97}