upleveled / preflight

Preflight checks for your UpLeveled projects before you submit 🚀
https://www.npmjs.com/package/@upleveled/preflight
MIT License
14 stars 3 forks source link

Add outdated lockfile check #438

Open more-no opened 1 year ago

more-no commented 1 year ago

When I tried to deploy on Netlify this is what happened:

7:52:49 PM: Installing npm packages using pnpm version 8.3.1
7:52:49 PM: Lockfile is up to date, resolution step is skipped
7:52:49 PM:  ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with frozen-lockfile because pnpm-lock.yaml is not up to date with package.json
7:52:49 PM: Note that in CI environments this setting is true by default. If you still need to run install in such cases, use pnpm install --no-frozen-lockfile
7:52:49 PM: Error during pnpm install
7:52:49 PM: Build was terminated: dependency_installation script returned non-zero exit code: 1
7:52:50 PM: Failing build: Failed to install dependencies

It wasn' t expecting it because Preflight didn't gave any error and it is supposed to be checking also dependency problems.

So it would be nice if in the future Preflight were able to also find the error that Netlify chatched.

BTW my problem was easily solved with: pnpm install --no-frozen-lockfile

After that I was able to deploy.

karlhorky commented 1 year ago

@Josehower mentioned today that there were some checks that Preflight already does which sometimes catch outdated lockfiles

But there are some situations which we still do not catch

ProchaLu commented 1 year ago

I've reviewed the Preflight code, and it looks like we're not checking anything with the pnpm-lock.yaml file. We do have checks for package.json:

ProchaLu commented 1 year ago

After some research, I found a package called package-lock-utd that checks outdated package-lock.json files. I was able to extract the code and refactor it to check if the package.json file and pnpm-lock.yaml are matching. I created a script and tested it in a Portfolio project.

The script works as expected:

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

function existsAndIsFileSync(filePath) {
  try {
    const stat = fs.statSync(filePath);
    return stat.isFile();
  } catch (error) {
    return false;
  }
}

function readFileContent(filePath) {
  try {
    const data = fs.readFileSync(filePath, 'utf8');
    return { success: true, data };
  } catch (error) {
    return { success: false };
  }
}

const packageJsonPath = path.join(process.cwd(), 'package.json');
const pnpmLockYamlPath = path.join(process.cwd(), 'pnpm-lock.yaml');
let shouldRestorePackageLock = false;

if (!existsAndIsFileSync(packageJsonPath)) {
  console.log("File 'package.json' is missing");
  process.exit(1);
}

if (!existsAndIsFileSync(pnpmLockYamlPath)) {
  console.log("File 'package-lock.yaml' is missing");
  process.exit(1);
}

const originalPackageLockReadResult = readFileContent(pnpmLockYamlPath);

if (!originalPackageLockReadResult.success) {
  console.log("Failed to read 'package-lock.yaml'");
  process.exit(1);
}

try {
  execSync('pnpm install');
  shouldRestorePackageLock = true;
} catch {
  console.log(
    'Something went wrong generating the updated package-lock.yaml needed for comparison. This could be a problem with pnpm',
  );
  process.exit(1);
} finally {
  if (shouldRestorePackageLock) {
    // Restore the original package-lock.json when the process exits.
    process.on('exit', () => {
      fs.writeFileSync(
        pnpmLockYamlPath,
        originalPackageLockReadResult.data || '',
      );
    });
  }
}

const updatedPackageLockReadResult = readFileContent(pnpmLockYamlPath);

if (!updatedPackageLockReadResult.success) {
  console.log("Failed to read the updated 'package-lock.yaml'");
  process.exit(1);
}

if (originalPackageLockReadResult.data !== updatedPackageLockReadResult.data) {
  console.log('package-lock.yaml is not up to date. Run "pnpm install"');
  process.exit(1);
}

console.log('package-lock.yaml is up to date');

Alternative Solutions

Instead of using pnpm install, maybe we can use a diff tool to compare the contents of the package.json and pnpm-lock.yaml files.

karlhorky commented 1 year ago

not sure if these pnpm install flags help (also 250ms on my M1, maybe too slow / expensive)

# lockfile needs no updates (exit code 0)
$ pnpm install --lockfile-only --frozen-lockfile
Done in 229ms

# lockfile needs an update (exit code 1)
$ pnpm install --lockfile-only --frozen-lockfile
 ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with package.json

Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile"

    Failure reason:
    specifiers in the lockfile ({"@types/prettier":"2.7.3","eslint-config-upleveled":"5.0.4","stylelint":"15.10.3","typescript":"5.2.2"}) don't match specs in package.json ({"@babel/eslint-parser":"^7.22.15","@next/eslint-plugin-next":"^13.5.2","@types/eslint":"^8.44.2","@types/node":">=20.6.3","@types/react":"^18.2.22","@types/react-dom":"^18.2.7","@typescript-eslint/eslint-plugin":"^6.7.2","@typescript-eslint/parser":"^6.7.2","eslint":"^8.49.0","eslint-import-resolver-typescript":"^3.6.0","eslint-plugin-import":"^2.28.1","eslint-plugin-jsx-a11y":"^6.7.1","eslint-plugin-jsx-expressions":"^1.3.1","eslint-plugin-react":"^7.33.2","eslint-plugin-react-hooks":"^4.6.0","eslint-plugin-security":"^1.7.1","eslint-plugin-sonarjs":"^0.21.0","eslint-plugin-testing-library":"^6.0.1","eslint-plugin-unicorn":"^48.0.1","eslint-plugin-upleveled":"^2.1.9","typescript":"5.2.2","@types/prettier":"2.7.3","eslint-config-upleveled":"5.0.4","stylelint":"15.10.3"})