Skip to content

Commit

Permalink
Merge pull request #3 from sparksuite/initial-implementation
Browse files Browse the repository at this point in the history
Initial implementation
  • Loading branch information
WesCossick authored Mar 1, 2021
2 parents 2b0de5f + 3e70aa3 commit 6a53a4c
Show file tree
Hide file tree
Showing 16 changed files with 801 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
coverage/
dist/
yarn-error.log*
2 changes: 1 addition & 1 deletion eslint.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/prefer-regexp-exec": "off",
Expand Down
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
"files": [
"dist/"
],
"bin": {
"rugged": "./dist/index.js"
},
"scripts": {
"dev": "yarn install --frozen-lockfile && yarn compile && yarn --cwd ./website install --frozen-lockfile",
"test": "yarn compile && for directory in ./test-projects/*/ ; do (yarn --cwd \"$directory\" test); done",
"lint": "eslint --ext .js,.ts,.jsx,.tsx ./website && prettier --check '**/*.{ts,js,tsx,jsx,json,css,html,yml}'",
"format": "eslint --fix --ext .js,.ts,.jsx,.tsx ./website && prettier --write '**/*.{ts,js,tsx,jsx,json,css,html,yml}'",
"clean": "git clean -X -d --force && find . -type d -empty -delete",
"precompile": "rm -rf dist/",
"compile": "tsc --project tsconfig.build.json"
"compile": "tsc --project tsconfig.build.json",
"postcompile": "chmod +x dist/index.js"
},
"repository": {
"type": "git",
Expand All @@ -33,9 +37,18 @@
"url": "https://github.com/sparksuite/rugged/issues"
},
"homepage": "https://github.com/sparksuite/rugged",
"dependencies": {},
"dependencies": {
"chalk": "^4.1.0",
"execa": "^5.0.0",
"glob": "^7.1.6",
"listr": "^0.14.3",
"tmp": "^0.2.1"
},
"devDependencies": {
"@types/glob": "^7.1.3",
"@types/listr": "^0.14.2",
"@types/node": "^14.14.25",
"@types/tmp": "^0.2.0",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"eslint": "^7.19.0",
Expand Down
124 changes: 121 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,122 @@
// TODO: A description
const rugged = 'Hello world';
#!/usr/bin/env node

export = rugged;
// Imports
import path from 'path';
import chalk from 'chalk';
import glob from 'glob';
import Listr from 'listr';
import execa from 'execa';
import { HandledError, yarnErrorCatcher } from './utils/errors';
import configure from './utils/configure';
import verify from './utils/verify';
import printHeader from './utils/print-header';
import installDependencies from './steps/install-dependencies';
import injectRootPackage from './steps/inject-root-package';
import testProjects from './steps/test-projects';

// Initialize finish function
let finishUp = () => {};

// Initialize object to store the final result
export interface FinalResult {
errorEncountered: boolean;
failedTests: {
project: string;
output: string;
}[];
}

const finalResult: FinalResult = {
errorEncountered: false,
failedTests: [],
};

// Wrap everything in a self-executing async function
(async () => {
// Get the configuration
const configuration = await configure();

// Verification
const packageFile = verify.packageFile();
const absolutePath = verify.testProjects(configuration);

// Determine test project paths
const testProjectPaths = glob.sync(`${absolutePath}/*/`);

// Define the actual finish function
finishUp = async () => {
// Print section header
printHeader('Resetting projects');

// Remove packaged version and add linked version
const tasks = new Listr(
testProjectPaths.map((testProjectPath) => ({
title: path.basename(testProjectPath),
task: async () => {
await execa('yarn', [`remove`, packageFile.name], {
cwd: testProjectPath,
}).catch((error) => {
if (error.toString().includes(`This module isn't specified in a package.json file`)) {
return;
}

return yarnErrorCatcher(error);
});

await execa('yarn', [`add`, `link:../..`], {
cwd: testProjectPath,
}).catch(yarnErrorCatcher);
},
})),
{
concurrent: true,
exitOnError: false,
}
);

await tasks.run();

// Loop over each failed test
for (const failedTest of finalResult.failedTests) {
// Print section header
console.log(`\n${chalk.inverse(chalk.red(chalk.bold(` Output from: ${failedTest.project} `)))}\n`);

// Print output
console.log(failedTest.output.trim());
}

// Final newline
console.log();

// Exit accordingly
if (finalResult.errorEncountered || finalResult.failedTests.length) {
process.exit(1);
}
};

// Trigger each step
await installDependencies(packageFile, testProjectPaths);
await injectRootPackage(packageFile, testProjectPaths);
await testProjects(testProjectPaths, finalResult);
})()
.catch((error) => {
// Remember that we encountered an error
finalResult.errorEncountered = true;

// Catch already-handled errors
if (error instanceof HandledError) {
return;
}

// Handle unexpected errors
console.error(chalk.italic(chalk.red('\nRugged encountered an unexpected error (see below):\n')));
console.error(chalk.red(error.stack?.trim() || error.toString()?.trim()));
console.error(
chalk.italic(
chalk.red(
`\nYou may want to report this: ${chalk.underline(`https://github.com/sparksuite/rugged/issues/new`)}\n`
)
)
);
})
.finally(() => finishUp());
85 changes: 85 additions & 0 deletions src/steps/inject-root-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Imports
import execa from 'execa';
import Listr from 'listr';
import path from 'path';
import { HandledError, yarnErrorCatcher } from '../utils/errors';
import printHeader from '../utils/print-header';
import { PackageFile } from '../utils/verify';
import tmp from 'tmp';

/** Installs dependencies into the root project and test projects */
export default async function injectRootPackage(packageFile: PackageFile, testProjectPaths: string[]) {
// Print section header
printHeader(`Injecting ${packageFile.name}`);

// Set up the tasks
const tasks = new Listr([
{
title: `Compiling`,
task: () => execa('yarn', ['compile']).catch(yarnErrorCatcher),
},
{
title: `Packaging`,
task: (ctx) => {
const tmpDir = tmp.dirSync({
unsafeCleanup: true,
});

ctx.packagePath = path.join(tmpDir.name, `package-${Math.random().toString(36).substring(7)}.tgz`);

return execa('yarn', [`pack`, `--filename`, ctx.packagePath]).catch(yarnErrorCatcher);
},
},
{
title: 'Removing linked version',
task: () =>
new Listr(
testProjectPaths.map((testProjectPath) => ({
title: path.basename(testProjectPath),
task: async () => {
await execa('yarn', [`remove`, packageFile.name], {
cwd: testProjectPath,
}).catch((error) => {
if (error.toString().includes(`This module isn't specified in a package.json file`)) {
return;
}

return yarnErrorCatcher(error);
});

await execa('yarn', [`unlink`, packageFile.name], {
cwd: testProjectPath,
}).catch(yarnErrorCatcher);
},
})),
{
concurrent: true,
exitOnError: false,
}
),
},
{
title: 'Injecting packaged version',
task: (ctx) =>
new Listr(
testProjectPaths.map((testProjectPath) => ({
title: path.basename(testProjectPath),
task: () =>
execa('yarn', [`add`, `file:${ctx.packagePath}`], {
cwd: testProjectPath,
}).catch(yarnErrorCatcher),
})),
{
concurrent: true,
exitOnError: false,
}
),
},
]);

// Run the tasks, catching any errors
await tasks.run().catch(() => {
console.log();
throw new HandledError();
});
}
40 changes: 40 additions & 0 deletions src/steps/install-dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Imports
import execa from 'execa';
import Listr from 'listr';
import path from 'path';
import { HandledError, yarnErrorCatcher } from '../utils/errors';
import printHeader from '../utils/print-header';
import { PackageFile } from '../utils/verify';

/** Installs dependencies into the root project and test projects */
export default async function installDependencies(packageFile: PackageFile, testProjectPaths: string[]) {
// Print section header
printHeader('Installing dependencies');

// Set up the tasks
const tasks = new Listr(
[
{
title: packageFile.name,
task: () => execa('yarn', [`install`, `--frozen-lockfile`, `--prefer-offline`]).catch(yarnErrorCatcher),
},
...testProjectPaths.map((testProjectPath) => ({
title: `Project: ${path.basename(testProjectPath)}`,
task: () =>
execa('yarn', [`install`, `--frozen-lockfile`, `--prefer-offline`], {
cwd: testProjectPath,
}).catch(yarnErrorCatcher),
})),
],
{
concurrent: true,
exitOnError: false,
}
);

// Run the tasks, catching any errors
await tasks.run().catch(() => {
console.log();
throw new HandledError();
});
}
44 changes: 44 additions & 0 deletions src/steps/test-projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Imports
import execa from 'execa';
import Listr from 'listr';
import path from 'path';
import { FinalResult } from '..';
import { HandledError } from '../utils/errors';
import printHeader from '../utils/print-header';

/** Installs dependencies into the root project and test projects */
export default async function testProjects(testProjectPaths: string[], finalResult: FinalResult) {
// Print section header
printHeader('Testing projects');

// Set up the tasks
const tasks = new Listr(
testProjectPaths.map((testProjectPath) => ({
title: path.basename(testProjectPath),
task: () =>
execa('yarn', [`test`], {
cwd: testProjectPath,
all: true,
}).catch((error) => {
// Add to final result
finalResult.failedTests.push({
project: path.basename(testProjectPath),
output: error.all ?? 'No output...',
});

// Throw error that Listr will pick up
throw new Error('Output will be printed below');
}),
})),
{
concurrent: true,
exitOnError: false,
}
);

// Run the tasks, catching any errors
await tasks.run().catch(() => {
console.log();
throw new HandledError();
});
}
12 changes: 12 additions & 0 deletions src/utils/configure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Define what configuration looks like
export interface Configuration {
testProjectsDirectory: string;
}

/** Construct the configuration object */
export default async function configure(): Promise<Configuration> {
// Return
return {
testProjectsDirectory: 'test-projects',
};
}
20 changes: 20 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Imports
import execa from 'execa';

/** This error indicates it was expected and already handled */
export class HandledError extends Error {
constructor(message?: string) {
super(message);
this.name = 'HandledError';
}
}

/** Yarn error catcher for Listr */
export const yarnErrorCatcher = (error: execa.ExecaError<string>) => {
throw new Error(
error.stderr
.split('\n')
.map((line) => line.replace(/^error /, ''))
.join('\n')
);
};
7 changes: 7 additions & 0 deletions src/utils/print-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Imports
import chalk from 'chalk';

/** Print a stylized section header */
export default function printHeader(header: string) {
console.log(`\n${chalk.inverse(chalk.bold(` ${header} `))}\n`);
}
Loading

0 comments on commit 6a53a4c

Please sign in to comment.