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}