Skip to content
This repository has been archived by the owner on Dec 18, 2021. It is now read-only.

Commit

Permalink
feat(engines): added linting of engines restrictions and visibility t…
Browse files Browse the repository at this point in the history
…hrough a badge

also took a big step toward making other enhancers simpler to add going forward
  • Loading branch information
travi committed Nov 23, 2021
1 parent 9cc8644 commit 4e9bef3
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 31 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"test:unit:base": "DEBUG=any mocha 'src/**/*-test.js'",
"lint:peer": "npm ls >/dev/null",
"lint:gherkin": "gherkin-lint",
"test:integration": "preview && run-s 'test:integration:base -- --profile noWip'",
"pretest:integration:base": "preview",
"test:integration": "run-s 'test:integration:base -- --profile noWip'",
"test:integration:base": "DEBUG=any cucumber-js test/integration --profile base",
"test:integration:debug": "NODE_OPTIONS=--enable-source-maps DEBUG=test run-s test:integration",
"test:integration:wip": "run-s 'test:integration:base -- --profile wip'",
Expand Down
61 changes: 61 additions & 0 deletions src/enhancers/apply-enhancers-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {assert} from 'chai';
import any from '@travi/any';
import sinon from 'sinon';
import applyEnhancers from './apply';

suite('enhancers', () => {
const results = any.simpleObject();
const projectRoot = any.string();

test('that an enhancer that matches the project is executed', async () => {
const lift = sinon.stub();
const anotherLift = sinon.stub();
const test = sinon.stub();
const otherLift = sinon.spy();
const liftNextSteps = any.listOf(any.simpleObject);
const liftResults = {nextSteps: liftNextSteps};
const anotherLiftResults = any.simpleObject();
test.withArgs({projectRoot}).resolves(true);
lift.withArgs({results, projectRoot}).resolves(liftResults);
anotherLift.withArgs({results: {...results, ...liftResults}, projectRoot}).resolves(anotherLiftResults);

const enhancerResults = await applyEnhancers({
results,
projectRoot,
enhancers: {
[any.word()]: {test, lift},
[any.word()]: {test: () => Promise.resolve(false), lift: otherLift},
[any.word()]: {test, lift: anotherLift}
}
});

assert.deepEqual(enhancerResults, {...results, ...liftResults, ...anotherLiftResults});
assert.calledWith(lift, {results, projectRoot});
assert.notCalled(otherLift);
});

test('that an enhancer error rejects the enhancer application', async () => {
const error = new Error('from test');

try {
await applyEnhancers({
results,
projectRoot,
enhancers: {
[any.word()]: {
test: () => Promise.resolve(true),
lift: () => Promise.reject(error)
}
}
});

throw new Error('applying enhancers should have thrown an error');
} catch (e) {
assert.equal(e, error);
}
});

test('that no liftEnhancers are applied if none are provided', async () => {
assert.deepEqual(await applyEnhancers({results}), results);
});
});
20 changes: 20 additions & 0 deletions src/enhancers/apply.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import deepmerge from 'deepmerge';
import {info} from '@travi/cli-messages';

export default async function ({results, enhancers = {}, projectRoot}) {
info('Applying Enhancers');

return Object.values(enhancers)
.reduce(async (acc, enhancer) => {
if (await enhancer.test({projectRoot})) {
const previousResults = await acc;

return deepmerge(
previousResults,
await enhancer.lift({results: previousResults, projectRoot})
);
}

return acc;
}, results);
}
49 changes: 49 additions & 0 deletions src/enhancers/engines-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {promises as fs} from 'fs';

import {assert} from 'chai';
import any from '@travi/any';
import sinon from 'sinon';

import {test as predicate, lift} from './engines';

suite('engines enhancer', () => {
let sandbox;
const projectRoot = any.string();

setup(() => {
sandbox = sinon.createSandbox();

sandbox.stub(fs, 'readFile');
});

teardown(() => sandbox.restore());

test('that the predicate returns `true` when `engines.node` is defined', async () => {
fs.readFile.withArgs(`${projectRoot}/package.json`, 'utf8').resolves(JSON.stringify({engines: {node: any.word()}}));

assert.isTrue(await predicate({projectRoot}));
});

test('that the predicate returns `false` when `engines.node` is not defined', async () => {
fs.readFile.resolves(JSON.stringify({engines: {}}));

assert.isFalse(await predicate({projectRoot}));
});

test('that the predicate returns `false` when `engines` is not defined', async () => {
fs.readFile.resolves(JSON.stringify({}));

assert.isFalse(await predicate({projectRoot}));
});

test('that the lifter returns the details for linting and communicating engines restrictions', async () => {
const projectName = any.word();
fs.readFile.withArgs(`${projectRoot}/package.json`, 'utf8').resolves(JSON.stringify({name: projectName}));

const {scripts, badges, devDependencies} = await lift({projectRoot});

assert.equal(scripts['lint:engines'], 'ls-engines');
assert.deepEqual(devDependencies, ['ls-engines']);
assert.deepEqual(badges.consumer.node, {img: `https://img.shields.io/node/v/${projectName}.svg`, text: 'node'});
});
});
17 changes: 17 additions & 0 deletions src/enhancers/engines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {promises as fs} from 'fs';

export async function test({projectRoot}) {
const {engines} = JSON.parse(await fs.readFile(`${projectRoot}/package.json`, 'utf8'));

return !!engines?.node;
}

export async function lift({projectRoot}) {
const {name} = JSON.parse(await fs.readFile(`${projectRoot}/package.json`, 'utf8'));

return {
devDependencies: ['ls-engines'],
scripts: {'lint:engines': 'ls-engines'},
badges: {consumer: {node: {img: `https://img.shields.io/node/v/${name}.svg`, text: 'node'}}}
};
}
12 changes: 10 additions & 2 deletions src/lift-test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import * as huskyLifter from '@form8ion/husky';
import * as eslint from '@form8ion/eslint';
import deepmerge from 'deepmerge';

import sinon from 'sinon';
import any from '@travi/any';
import {assert} from 'chai';

import * as enhancers from './enhancers/apply';
import * as enginesEnhancer from './enhancers/engines';
import * as packageLifter from './package';
import * as packageManagerResolver from './package-manager';
import lift from './lift';
Expand Down Expand Up @@ -36,6 +40,7 @@ suite('lift', () => {
sandbox.stub(eslint, 'lift');
sandbox.stub(packageManagerResolver, 'default');
sandbox.stub(huskyLifter, 'lift');
sandbox.stub(enhancers, 'default');

huskyLifter.lift.withArgs({projectRoot, packageManager}).resolves(huskyLiftResults);
packageManagerResolver.default.withArgs({projectRoot, packageManager: manager}).resolves(packageManager);
Expand All @@ -46,17 +51,20 @@ suite('lift', () => {
test('that results specific to js projects are lifted', async () => {
const scope = any.word();
const eslintLiftResults = {...any.simpleObject(), devDependencies: any.listOf(any.word)};
const enhancerResults = any.simpleObject();
eslint.lift.withArgs({configs: eslintConfigs, projectRoot}).resolves(eslintLiftResults);
enhancers.default.withArgs({results, enhancers: [enginesEnhancer], projectRoot}).resolves(enhancerResults);

const liftResults = await lift({projectRoot, results, configs: {eslint: {scope}}});

assert.deepEqual(liftResults, {});
assert.deepEqual(liftResults, enhancerResults);
assert.calledWith(
packageLifter.default,
deepmerge.all([
{projectRoot, scripts, tags, dependencies, devDependencies, packageManager},
huskyLiftResults,
eslintLiftResults
eslintLiftResults,
enhancerResults
])
);
});
Expand Down
26 changes: 12 additions & 14 deletions src/lift.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
import {info} from '@travi/cli-messages';
import deepmerge from 'deepmerge';
import {info} from '@travi/cli-messages';
import {lift as liftHusky} from '@form8ion/husky';
import {lift as liftEslint} from '@form8ion/eslint';

import applyEnhancers from './enhancers/apply';
import * as enginesEnhancer from './enhancers/engines';
import liftPackage from './package';
import resolvePackageManager from './package-manager';

export default async function ({
projectRoot,
results: {scripts, tags, eslintConfigs, dependencies, devDependencies, packageManager: manager}
}) {
export default async function ({projectRoot, results}) {
info('Lifting JavaScript-specific details');

const {scripts, tags, eslintConfigs, dependencies, devDependencies, packageManager: manager} = results;

const packageManager = await resolvePackageManager({projectRoot, packageManager: manager});

const huskyResults = await liftHusky({projectRoot, packageManager});
const eslintResults = await liftEslint({projectRoot, configs: eslintConfigs});
const enhancerResults = await applyEnhancers({results, enhancers: [enginesEnhancer], projectRoot});

await liftPackage(
deepmerge.all([
{
projectRoot,
scripts,
tags,
dependencies,
devDependencies,
packageManager
},
{projectRoot, scripts, tags, dependencies, devDependencies, packageManager},
enhancerResults,
huskyResults,
eslintResults
])
);

return {};
return enhancerResults;
}
7 changes: 1 addition & 6 deletions src/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,5 @@ export default async function ({
info('Installing dependencies');

await installDependencies(dependencies || [], PROD_DEPENDENCY_TYPE, projectRoot, packageManager);
await installDependencies(
[...devDependencies || []],
DEV_DEPENDENCY_TYPE,
projectRoot,
packageManager
);
await installDependencies([...devDependencies || []], DEV_DEPENDENCY_TYPE, projectRoot, packageManager);
}
10 changes: 10 additions & 0 deletions test/integration/features/engines.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Feature: Engines

Scenario: Engines defined for node
Given a definition exists for engines.node
And an "npm" lockfile exists
And husky v5 is installed
When the scaffolder results are processed
Then the script is added for ensuring the node engines requirement is met
And ls-engines is added as a dependency
And the engines badge is added to the consumer group
4 changes: 3 additions & 1 deletion test/integration/features/step_definitions/common-steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ When('the scaffolder results are processed', async function () {
await fs.writeFile(
`${process.cwd()}/package.json`,
JSON.stringify({
name: this.projectName,
scripts: this.existingScripts,
keywords: this.existingKeywords
keywords: this.existingKeywords,
...this.enginesNode && {engines: {node: this.enginesNode}}
})
);

Expand Down
21 changes: 21 additions & 0 deletions test/integration/features/step_definitions/dependencies-steps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Then} from '@cucumber/cucumber';
import td from 'testdouble';

function escapeSpecialCharacters(string) {
return string.replace(/[.*+?^$\-{}()|[\]\\]/g, '\\$&');
}

export function assertDevDependencyIsInstalled(execa, dependencyName) {
const {DEV_DEPENDENCY_TYPE} = require('@form8ion/javascript-core');

td.verify(
execa(td.matchers.contains(
new RegExp(`(npm install|yarn add).*${escapeSpecialCharacters(dependencyName)}.*${DEV_DEPENDENCY_TYPE}`)
)),
{ignoreExtraArgs: true}
);
}

Then('ls-engines is added as a dependency', async function () {
assertDevDependencyIsInstalled(this.execa, 'ls-engines');
});
19 changes: 19 additions & 0 deletions test/integration/features/step_definitions/engines-steps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Given, Then} from '@cucumber/cucumber';
import any from '@travi/any';
import {assert} from 'chai';

Given('a definition exists for engines.node', async function () {
this.enginesNode = any.word();
});

Then('the engines badge is added to the consumer group', async function () {
const {badges} = this.results;

assert.deepEqual(
badges.consumer.node,
{
img: `https://img.shields.io/node/v/${this.projectName}.svg`,
text: 'node'
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {Given, Then} from '@cucumber/cucumber';
import {assert} from 'chai';
import any from '@travi/any';
import {fileExists} from '@form8ion/core';
import td from 'testdouble';

import {assertDevDependencyIsInstalled} from './dependencies-steps';

const pathToYamlConfig = `${process.cwd()}/.eslintrc.yml`;
const eslintConfigScope = `@${any.word()}`;
Expand Down Expand Up @@ -58,8 +59,5 @@ Then('dependencies are defined for the additional configs', async function () {
return `${this.eslintConfigScope}/eslint-config-${config.name}`;
});

td.verify(
this.execa(td.matchers.contains(new RegExp(`(npm install|yarn add).*${additionalConfigPackageNames.join(' ')}`))),
{ignoreExtraArgs: true}
);
assertDevDependencyIsInstalled(this.execa, additionalConfigPackageNames.join(' '));
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,5 @@ Given('an {string} lockfile exists', async function (packageManager) {
}

this.packageManager = packageManager;
this.projectName = any.word();
});

// TODO: move the td.verify for package installation here
6 changes: 6 additions & 0 deletions test/integration/features/step_definitions/scripts-steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ Then('the additional scripts exist', async function () {

Object.entries(this.scriptsResults).forEach(([scriptName, script]) => assert.equal(scripts[scriptName], script));
});

Then('the script is added for ensuring the node engines requirement is met', async function () {
const {scripts} = JSON.parse(await fs.readFile(`${process.cwd()}/package.json`, 'utf8'));

assert.equal(scripts['lint:engines'], 'ls-engines');
});

0 comments on commit 4e9bef3

Please sign in to comment.