forked from tester-army/e2e
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall-scripts.ts
More file actions
104 lines (95 loc) · 4.62 KB
/
Copy pathinstall-scripts.ts
File metadata and controls
104 lines (95 loc) · 4.62 KB
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
/**
* Finds the packages a consumer's install would run a script for, in the
* dependency tree of a package as installed on this machine.
*
* pnpm 11 and later fail an install on any dependency build script the
* project has not decided on (`ERR_PNPM_IGNORED_BUILDS`), so one such
* package anywhere under a published package breaks `pnpm install` for every
* new user. npm runs a package's `preinstall`, `install`, and `postinstall`,
* and `node-gyp rebuild` for a `binding.gyp` with no install script of its
* own; any of them counts.
*
* The walk follows `dependencies`, `optionalDependencies`, and the
* `peerDependencies` an install adds unasked (npm 7 and later, pnpm), the way
* Node.js resolves them from each package's real path. An optional
* dependency this platform skipped (another OS's native binary) is not
* there to read.
*/
import { existsSync, readFileSync, realpathSync } from 'node:fs';
import { dirname, join } from 'node:path';
const INSTALL_HOOKS = ['preinstall', 'install', 'postinstall'] as const;
interface Manifest {
name: string;
version?: string;
scripts?: Record<string, string>;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
}
export interface InstallScript {
/** `root > dependency > ... > package@version`. */
readonly chain: string;
/** What an install would run: the scripts, or `node-gyp rebuild`. */
readonly runs: string;
}
function readManifest(dir: string): Manifest {
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as Manifest;
}
/** The real directory of `name` as Node.js resolves it from `fromDir`, or undefined when it is not installed. */
function locate(name: string, fromDir: string): string | undefined {
for (let dir = fromDir; ; dir = dirname(dir)) {
const candidate = join(dir, 'node_modules', name);
if (existsSync(join(candidate, 'package.json'))) return realpathSync(candidate);
if (dirname(dir) === dir) return undefined;
}
}
/** What installing the package in `dir` runs, or undefined when nothing. */
function installRuns(dir: string, manifest: Manifest): string | undefined {
const hooks = INSTALL_HOOKS.filter((hook) => manifest.scripts?.[hook] !== undefined);
if (hooks.length > 0) return hooks.map((hook) => `${hook}: ${manifest.scripts![hook]}`).join('; ');
return existsSync(join(dir, 'binding.gyp')) ? 'node-gyp rebuild (binding.gyp)' : undefined;
}
/**
* The dependencies an install of `manifest` brings in, each with whether it
* may be absent. A name in `optionalDependencies` is optional even when
* `dependencies` names it too, as npm reads it.
*/
function installedDependencies(manifest: Manifest): [name: string, optional: boolean][] {
const optional = new Set(Object.keys(manifest.optionalDependencies ?? {}));
const peers = Object.keys(manifest.peerDependencies ?? {}).filter((name) => manifest.peerDependenciesMeta?.[name]?.optional !== true);
const required = [...Object.keys(manifest.dependencies ?? {}), ...peers].filter((name) => !optional.has(name));
return [
...[...new Set(required)].map((name): [string, boolean] => [name, false]),
...[...optional].map((name): [string, boolean] => [name, true]),
];
}
/**
* The package in `packageDir`, then every package in its installed
* dependency tree, whose install runs a script. Throws when a required
* dependency is not installed: the tree cannot be read.
*/
export function findInstallScripts(packageDir: string): InstallScript[] {
const root = readManifest(packageDir);
const ownRuns = installRuns(packageDir, root);
const found: InstallScript[] = ownRuns === undefined ? [] : [{ chain: root.name, runs: ownRuns }];
const seen = new Set<string>([realpathSync(packageDir)]);
const queue: { dir: string; manifest: Manifest; chain: string }[] = [{ dir: realpathSync(packageDir), manifest: root, chain: root.name }];
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
for (const [name, optional] of installedDependencies(next.manifest)) {
const dir = locate(name, next.dir);
if (dir === undefined) {
if (optional) continue;
throw new Error(`${next.chain} > ${name} is not installed; run pnpm install`);
}
if (seen.has(dir)) continue;
seen.add(dir);
const manifest = readManifest(dir);
const chain = `${next.chain} > ${manifest.name}@${manifest.version ?? '?'}`;
const runs = installRuns(dir, manifest);
if (runs !== undefined) found.push({ chain, runs });
queue.push({ dir, manifest, chain });
}
}
return found;
}