File size: 7,129 Bytes
704e84f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2c232b
704e84f
 
 
 
a2c232b
 
704e84f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2c232b
704e84f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2c232b
 
 
 
 
 
 
704e84f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env node

import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { NPM_INSTALL_POLICY_CHECK_COMMAND } from './npm-install-policy.mjs';

export const LOCAL_DEPENDENCY_INSTALL_COMMAND =
    `npm run install-scripts:check && ${NPM_INSTALL_POLICY_CHECK_COMMAND} && npm ci --strict-allow-scripts && npm run dependencies:check`;

export function inspectDependencyInstallation(cwd = process.cwd()) {
    const root = resolve(cwd);
    const nodeModulesPath = join(root, 'node_modules');
    const rootLockfilePath = join(root, 'package-lock.json');
    const hiddenLockfilePath = join(nodeModulesPath, '.package-lock.json');
    if (!existsSync(nodeModulesPath)) return buildFailure('node_modules_missing');

    const rootLockfile = readLockfile(rootLockfilePath, true);
    if (!rootLockfile.ok) return buildFailure(rootLockfile.reason);
    const hiddenLockfile = readLockfile(hiddenLockfilePath, false);
    if (!hiddenLockfile.ok) return buildFailure(hiddenLockfile.reason);

    const directDependencies = collectDirectDependencies(rootLockfile.value.packages['']);
    const rootLockMismatches = collectRootLockMismatches(directDependencies, rootLockfile.value.packages);
    if (rootLockMismatches.length) return buildFailure('root_lockfile_direct_package_missing', { directDependencies, rootLockMismatches });

    const hiddenLockMismatches = collectHiddenLockMismatches(
        directDependencies,
        rootLockfile.value.packages,
        hiddenLockfile.value.packages
    );
    if (hiddenLockMismatches.length) {
        return buildFailure('hidden_lockfile_package_mismatch', { directDependencies, hiddenLockMismatches });
    }

    const installedPackages = inspectDirectPackageManifests(nodeModulesPath, directDependencies, rootLockfile.value.packages);
    if (installedPackages.missingPackages.length) {
        return buildFailure('direct_package_missing', { directDependencies, ...installedPackages });
    }
    if (installedPackages.invalidPackages.length) {
        return buildFailure('direct_package_manifest_invalid', { directDependencies, ...installedPackages });
    }
    if (installedPackages.nameMismatches.length) {
        return buildFailure('direct_package_name_mismatch', { directDependencies, ...installedPackages });
    }
    if (installedPackages.versionMismatches.length) {
        return buildFailure('direct_package_version_mismatch', { directDependencies, ...installedPackages });
    }
    return { ok: true, directDependencies };
}

function readLockfile(path, isRootLockfile) {
    if (!existsSync(path)) return { ok: false, reason: isRootLockfile ? 'root_lockfile_missing' : 'hidden_lockfile_missing' };
    try {
        const value = JSON.parse(readFileSync(path, 'utf8'));
        if (!isLockfile(value, isRootLockfile)) {
            return { ok: false, reason: isRootLockfile ? 'root_lockfile_invalid' : 'hidden_lockfile_invalid' };
        }
        return { ok: true, value };
    } catch {
        return { ok: false, reason: isRootLockfile ? 'root_lockfile_invalid' : 'hidden_lockfile_invalid' };
    }
}

function isLockfile(value, requiresRootPackage) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
    if (!Number.isInteger(value.lockfileVersion) || !value.packages || typeof value.packages !== 'object') return false;
    return !requiresRootPackage || Boolean(value.packages[''] && typeof value.packages[''] === 'object');
}

function collectDirectDependencies(rootPackage) {
    const names = new Set();
    for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
        const dependencies = rootPackage?.[field];
        if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue;
        for (const name of Object.keys(dependencies)) names.add(name);
    }
    return [...names].sort();
}

function collectRootLockMismatches(directDependencies, packages) {
    return directDependencies.filter((name) => !readPackageVersion(packages, `node_modules/${name}`));
}

function collectHiddenLockMismatches(directDependencies, rootPackages, hiddenPackages) {
    return directDependencies.flatMap((name) => {
        const expected = readPackageVersion(rootPackages, `node_modules/${name}`);
        const actual = readPackageVersion(hiddenPackages, `node_modules/${name}`);
        return actual === expected ? [] : [{ name, expected, actual }];
    });
}

function inspectDirectPackageManifests(nodeModulesPath, directDependencies, rootPackages) {
    const missingPackages = [];
    const invalidPackages = [];
    const nameMismatches = [];
    const versionMismatches = [];
    for (const name of directDependencies) {
        const manifestPath = join(nodeModulesPath, name, 'package.json');
        if (!existsSync(manifestPath)) {
            missingPackages.push(name);
            continue;
        }
        const manifest = readPackageManifest(manifestPath);
        if (!manifest) {
            invalidPackages.push(name);
            continue;
        }
        if (manifest.name !== name) nameMismatches.push({ expected: name, actual: manifest.name });
        const expected = readPackageVersion(rootPackages, `node_modules/${name}`);
        if (manifest.version !== expected) versionMismatches.push({ name, expected, actual: manifest.version });
    }
    return { missingPackages, invalidPackages, nameMismatches, versionMismatches };
}

function readPackageManifest(path) {
    try {
        const manifest = JSON.parse(readFileSync(path, 'utf8'));
        if (!manifest || typeof manifest !== 'object' || typeof manifest.name !== 'string' || typeof manifest.version !== 'string') {
            return undefined;
        }
        return manifest;
    } catch {
        return undefined;
    }
}

function readPackageVersion(packages, packagePath, visited = new Set()) {
    if (visited.has(packagePath)) return undefined;
    visited.add(packagePath);
    const manifest = packages[packagePath];
    if (typeof manifest?.version === 'string' && manifest.version.length > 0) return manifest.version;
    if (!manifest?.link || typeof manifest.resolved !== 'string') return undefined;
    return readPackageVersion(packages, manifest.resolved, visited);
}

function buildFailure(reason, details = {}) {
    return {
        ok: false,
        reason,
        directDependencies: details.directDependencies || [],
        missingPackages: details.missingPackages || [],
        invalidPackages: details.invalidPackages || [],
        nameMismatches: details.nameMismatches || [],
        versionMismatches: details.versionMismatches || [],
        rootLockMismatches: details.rootLockMismatches || [],
        hiddenLockMismatches: details.hiddenLockMismatches || []
    };
}

function main() {
    const result = inspectDependencyInstallation();
    if (result.ok) return;
    console.error(JSON.stringify(result, null, 2));
    process.exitCode = 1;
}

if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main();