Skip to content

Commit

Permalink
Handle NODE_OPTIONS= in scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Jun 12, 2024
1 parent 7c87441 commit 2ec5189
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 21 deletions.
13 changes: 10 additions & 3 deletions packages/knip/@types/bash-parser/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ declare module '@ericcornelissen/bash-parser' {
prefix?: Prefix[];
};

type Prefix = {
expansion?: Expansion[];
};
export type Prefix = ExpansionNode | Assignment;

type LogicalExpression = {
type: 'LogicalExpression';
Expand Down Expand Up @@ -46,6 +44,10 @@ declare module '@ericcornelissen/bash-parser' {
body: CompoundList;
};

export type ExpansionNode = {
expansion: Expansion[];
};

type Expansion = {
type: 'CommandExpansion';
commandAST: AST;
Expand All @@ -56,6 +58,11 @@ declare module '@ericcornelissen/bash-parser' {
commands: Command[];
};

export type Assignment = {
type: 'AssignmentWord';
text: string;
};

export type Node = CompoundList | Command | LogicalExpression | If | For | Function_ | Pipeline;

export type AST = {
Expand Down
40 changes: 32 additions & 8 deletions packages/knip/src/binaries/bash-parser.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import parse from '@ericcornelissen/bash-parser';
import type { Node } from '@ericcornelissen/bash-parser';
import type { Assignment, ExpansionNode, Node, Prefix } from '@ericcornelissen/bash-parser';
import { debugLogObject } from '../util/debug.js';
import { toBinary } from '../util/protocols.js';
import * as FallbackResolver from './resolvers/fallback.js';
import KnownResolvers from './resolvers/index.js';
import { parseNodeArgs } from './resolvers/node.js';
import type { GetDependenciesFromScriptsOptions } from './types.js';
import { stripBinaryPath } from './util.js';
import { trimBinary } from './util.js';

// https://vorpaljs.github.io/bash-parser-playground/

type KnownResolver = keyof typeof KnownResolvers;

// Binaries that spawn a child process for the binary at first positional arg (and don't have custom resolver already)
const spawningBinaries = ['cross-env', 'retry-cli'];

const isExpansion = (node: Prefix): node is ExpansionNode => 'expansion' in node;

const isAssignment = (node: Prefix): node is Assignment => 'type' in node && node.type === 'AssignmentWord';

export const getBinariesFromScript = (script: string, options: GetDependenciesFromScriptsOptions) => {
if (!script) return [];

Expand All @@ -21,12 +30,13 @@ export const getBinariesFromScript = (script: string, options: GetDependenciesFr
nodes.flatMap(node => {
switch (node.type) {
case 'Command': {
const binary = node.name?.text ? stripBinaryPath(node.name.text) : node.name?.text;
const binary = node.name?.text ? trimBinary(node.name.text) : node.name?.text;

const commandExpansions =
node.prefix?.flatMap(
prefix => prefix.expansion?.filter(expansion => expansion.type === 'CommandExpansion') ?? []
) ?? [];
node.prefix
?.filter(isExpansion)
.map(prefix => prefix.expansion)
.flatMap(expansion => expansion.filter(expansion => expansion.type === 'CommandExpansion') ?? []) ?? [];

if (commandExpansions.length > 0) {
return commandExpansions.flatMap(expansion => getBinariesFromNodes(expansion.commandAST.commands)) ?? [];
Expand All @@ -42,17 +52,31 @@ export const getBinariesFromScript = (script: string, options: GetDependenciesFr
// Commands that precede other commands, try again with the rest
if (['!', 'test'].includes(binary)) return fromArgs(args);

const fromNodeOptions =
node.prefix
?.filter(isAssignment)
.filter(node => node.text.startsWith('NODE_OPTIONS='))
.flatMap(node => node.text.split('=')[1])
.map(arg => parseNodeArgs(arg.split(' ')))
.filter(args => args.require)
.map(arg => arg.require) ?? [];

if (binary in KnownResolvers) {
const resolver = KnownResolvers[binary as KnownResolver];
return resolver(binary, args, { ...options, fromArgs });
}

// Before using the fallback resolver, we need a way to bail out for scripts in environments like GitHub
if (spawningBinaries.includes(binary)) {
const command = script.replace(new RegExp(`.*${node.name?.text ?? binary}(\\s--\\s)?`), '');
return [toBinary(binary), ...getBinariesFromScript(command, options)];
}

// Before using the fallback resolver, we need a way to bail out for scripts in CI environments like GitHub
// Actions, which are provisioned with lots of unknown global binaries.
if (options.knownGlobalsOnly) return [];

// We apply a kitchen sink fallback resolver for everything else
return FallbackResolver.resolve(binary, args, { ...options, fromArgs });
return [...FallbackResolver.resolve(binary, args, { ...options, fromArgs }), ...fromNodeOptions];
}
case 'LogicalExpression':
return getBinariesFromNodes([node.left, node.right]);
Expand Down
7 changes: 5 additions & 2 deletions packages/knip/src/binaries/resolvers/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { compact } from '../../util/array.js';
import type { Resolver } from '../types.js';
import { tryResolveFilePath, tryResolveSpecifiers } from '../util.js';

export const resolve: Resolver = (_binary, args, { cwd }) => {
const parsed = parseArgs(args, {
export const parseNodeArgs = (args: string[]) =>
parseArgs(args, {
string: ['r'],
alias: { require: ['r', 'loader', 'experimental-loader', 'test-reporter', 'watch', 'import'] },
});

export const resolve: Resolver = (_binary, args, { cwd }) => {
const parsed = parseNodeArgs(args);
return compact([tryResolveFilePath(cwd, parsed._[0]), ...tryResolveSpecifiers(cwd, [parsed.require].flat())]);
};
4 changes: 2 additions & 2 deletions packages/knip/src/binaries/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const tryResolveFilePath = (cwd: string, specifier: string, acceptModuleS
return getPackageNameFromModuleSpecifier(specifier);
}
} else if (specifier.includes('node_modules/.bin')) {
return toBinary(stripBinaryPath(specifier));
return toBinary(trimBinary(specifier));
} else {
return getPackageNameFromFilePath(specifier);
}
Expand All @@ -29,7 +29,7 @@ export const stripVersionFromSpecifier = (specifier: string) => specifier.replac

const stripNodeModulesFromPath = (command: string) => command.replace(/^(\.\/)?node_modules\//, '');

export const stripBinaryPath = (command: string) =>
export const trimBinary = (command: string) =>
stripVersionFromSpecifier(
stripNodeModulesFromPath(command)
.replace(/^(\.bin\/)/, '')
Expand Down
13 changes: 7 additions & 6 deletions packages/knip/test/util/get-references-from-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,22 @@ test('getReferencesFromScripts (cross-env)', () => {
t('cross-env NODE_ENV=production program', ['bin:cross-env', 'bin:program']);
t('cross-env NODE_ENV=production program subcommand', ['bin:cross-env', 'bin:program']);
t('cross-env NODE_OPTIONS=--max-size=3072 program subcommand', ['bin:cross-env', 'bin:program']);
t('cross-env NODE_ENV=production node -r pkg/config ./script.js', ['bin:cross-env', 'pkg', js]);
t('cross-env NODE_OPTIONS="--loader pkg" knex', ['bin:cross-env', 'bin:knex', 'pkg']);
t('NODE_ENV=production cross-env -- program --cache', ['bin:cross-env', 'bin:program']);
});

test('getReferencesFromScripts (cross-env/node)', () => {
t('cross-env NODE_ENV=production node -r pkg/config ./script.js', ['bin:cross-env', js, 'pkg']);
t('cross-env NODE_ENV=production node -r node_modules/dotenv/config ./script.js', ['bin:cross-env', js, 'dotenv']);
t('cross-env NODE_ENV=production node -r esm script.js', ['bin:cross-env', js, 'esm']);
});

test('getReferencesFromScripts (nx)', () => {
t('nx run myapp:build:production', ['bin:nx']);
t('nx run-many -t build', ['bin:nx']);
t('nx exec -- esbuild main.ts --outdir=build', ['bin:nx', 'bin:esbuild', ts]);
});

test('getReferencesFromScripts (cross-env/node)', () => {
t('cross-env NODE_ENV=production node -r node_modules/dotenv/config ./script.js', ['bin:cross-env', 'dotenv', js]);
t('cross-env NODE_ENV=production node -r esm script.js', ['bin:cross-env', 'esm', js]);
});

test('getReferencesFromScripts (npm)', () => {
t('npm run script', ['bin:npm']);
t('npm run publish:latest -- --npm-tag=debug --no-push', ['bin:npm']);
Expand Down

0 comments on commit 2ec5189

Please sign in to comment.