File: specUtils.ts

package info (click to toggle)
node-corepack 0.24.0-5
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 262,916 kB
  • sloc: javascript: 94; makefile: 18; sh: 12
file content (127 lines) | stat: -rw-r--r-- 4,366 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import {UsageError}                                     from 'clipanion';
import fs                                               from 'fs';
import path                                             from 'path';
import semver                                           from 'semver';

import {Descriptor, Locator, isSupportedPackageManager} from './types';

const nodeModulesRegExp = /[\\/]node_modules[\\/](@[^\\/]*[\\/])?([^@\\/][^\\/]*)$/;

export function parseSpec(raw: unknown, source: string, {enforceExactVersion = true} = {}): Descriptor {
  if (typeof raw !== `string`)
    throw new UsageError(`Invalid package manager specification in ${source}; expected a string`);

  const match = raw.match(/^(?!_)([^@]+)(?:@(.+))?$/);
  if (match === null || (enforceExactVersion && (!match[2] || !semver.valid(match[2]))))
    throw new UsageError(`Invalid package manager specification in ${source} (${raw}); expected a semver version${enforceExactVersion ? `` : `, range, or tag`}`);

  if (!isSupportedPackageManager(match[1]))
    throw new UsageError(`Unsupported package manager specification (${match})`);

  return {
    name: match[1],
    range: match[2] ?? `*`,
  };
}

/**
 * Locates the active project's package manager specification.
 *
 * If the specification exists but doesn't match the active package manager,
 * an error is thrown to prevent users from using the wrong package manager,
 * which would lead to inconsistent project layouts.
 *
 * If the project doesn't include a specification file, we just assume that
 * whatever the user uses is exactly what they want to use. Since the version
 * isn't explicited, we fallback on known good versions.
 *
 * Finally, if the project doesn't exist at all, we ask the user whether they
 * want to create one in the current project. If they do, we initialize a new
 * project using the default package managers, and configure it so that we
 * don't need to ask again in the future.
 */
export async function findProjectSpec(initialCwd: string, locator: Locator, {transparent = false}: {transparent?: boolean} = {}): Promise<Descriptor> {
  // A locator is a valid descriptor (but not the other way around)
  const fallbackLocator = {name: locator.name, range: locator.reference};

  if (process.env.COREPACK_ENABLE_PROJECT_SPEC === `0`)
    return fallbackLocator;

  if (process.env.COREPACK_ENABLE_STRICT === `0`)
    transparent = true;

  while (true) {
    const result = await loadSpec(initialCwd);

    switch (result.type) {
      case `NoProject`:
      case `NoSpec`: {
        return fallbackLocator;
      }

      case `Found`: {
        if (result.spec.name !== locator.name) {
          if (transparent) {
            return fallbackLocator;
          } else {
            throw new UsageError(`This project is configured to use ${result.spec.name}`);
          }
        } else {
          return result.spec;
        }
      }
    }
  }
}

export type LoadSpecResult =
    | {type: `NoProject`, target: string}
    | {type: `NoSpec`, target: string}
    | {type: `Found`, target: string, spec: Descriptor};

export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
  let nextCwd = initialCwd;
  let currCwd = ``;

  let selection: {
    data: any;
    manifestPath: string;
  } | null = null;

  while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) {
    currCwd = nextCwd;
    nextCwd = path.dirname(currCwd);

    if (nodeModulesRegExp.test(currCwd))
      continue;

    const manifestPath = path.join(currCwd, `package.json`);
    if (!fs.existsSync(manifestPath))
      continue;

    const content = await fs.promises.readFile(manifestPath, `utf8`);

    let data;
    try {
      data = JSON.parse(content);
    } catch {}

    if (typeof data !== `object` || data === null)
      throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, manifestPath)}`);

    selection = {data, manifestPath};
  }

  if (selection === null)
    return {type: `NoProject`, target: path.join(initialCwd, `package.json`)};

  const rawPmSpec = selection.data.packageManager;
  if (typeof rawPmSpec === `undefined`)
    return {type: `NoSpec`, target: selection.manifestPath};

  return {
    type: `Found`,
    target: selection.manifestPath,
    spec: parseSpec(rawPmSpec, path.relative(initialCwd, selection.manifestPath)),
  };
}