1
0
Fork 0

remove(utils): use js-utils lib

This commit is contained in:
ssube 2020-03-31 19:44:02 -05:00
parent 186f6dfa98
commit 1121b112cc
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
25 changed files with 27 additions and 439 deletions

View File

@ -33,6 +33,8 @@
"author": "ssube",
"license": "MIT",
"devDependencies": {
"@apextoaster/js-utils": "^0.1.7",
"@apextoaster/js-yaml-schema": "^0.2.0",
"@istanbuljs/nyc-config-typescript": "1.0.1",
"@microsoft/api-documenter": "7.7.16",
"@microsoft/api-extractor": "7.7.12",

View File

@ -1,16 +1,13 @@
import { doesExist, NotFoundError } from '@apextoaster/js-utils';
import { Stream } from 'bunyan';
import { isString } from 'lodash';
import { LogLevel } from 'noicejs';
import { join } from 'path';
import { NotFoundError } from '../error/NotFoundError';
import { YamlParser } from '../parser/YamlParser';
import { readFile } from '../source';
import { doesExist } from '../utils';
import { CONFIG_ENV, CONFIG_SCHEMA } from './schema';
import { includeSchema } from './type/Include';
includeSchema.schema = CONFIG_SCHEMA;
export const CONFIG_ENV = 'SALTY_HOME';
export interface ConfigData {
data: {

View File

@ -1,14 +0,0 @@
import { DEFAULT_SAFE_SCHEMA, Schema } from 'js-yaml';
import { envType } from './type/Env';
import { includeType } from './type/Include';
import { regexpType } from './type/Regexp';
import { streamType } from './type/Stream';
export const CONFIG_ENV = 'SALTY_HOME';
export const CONFIG_SCHEMA = Schema.create([DEFAULT_SAFE_SCHEMA], [
envType,
includeType,
regexpType,
streamType,
]);

View File

@ -1,17 +0,0 @@
import { Type as YamlType } from 'js-yaml';
import { NotFoundError } from '../../error/NotFoundError';
export const envType = new YamlType('!env', {
kind: 'scalar',
resolve(name: string) {
if (Reflect.has(process.env, name)) {
return true;
} else {
throw new NotFoundError(`environment variable not found: ${name}`);
}
},
construct(name: string) {
return Reflect.get(process.env, name);
},
});

View File

@ -1,47 +0,0 @@
import { existsSync, readFileSync, realpathSync } from 'fs';
import { SAFE_SCHEMA, safeLoad, Type as YamlType } from 'js-yaml';
import { BaseError } from 'noicejs';
import { join } from 'path';
import { NotFoundError } from '../../error/NotFoundError';
// work around the circular dependency by setting the schema later
export const includeSchema = {
schema: SAFE_SCHEMA,
};
export const includeType = new YamlType('!include', {
kind: 'scalar',
resolve(path: string) {
try {
const canonical = resolvePath(path);
// throws in node 11+
if (existsSync(canonical)) {
return true;
} else {
throw new NotFoundError('included file does not exist');
}
} catch (err) {
throw new NotFoundError('included file does not exist', err);
}
},
construct(path: string): unknown {
try {
return safeLoad(readFileSync(resolvePath(path), {
encoding: 'utf-8',
}), {
schema: includeSchema.schema,
});
} catch (err) {
throw new BaseError('error including file', err);
}
},
});
export function resolvePath(path: string): string {
if (path[0] === '.') {
return realpathSync(join(__dirname, path));
} else {
return realpathSync(path);
}
}

View File

@ -1,21 +0,0 @@
import { Type as YamlType } from 'js-yaml';
import { isNil } from 'lodash';
import { InvalidArgumentError } from '../../error/InvalidArgumentError';
export const REGEXP_REGEXP = /^\/(.+)\/([gimsuy]*)$/;
export const regexpType = new YamlType('!regexp', {
kind: 'scalar',
resolve(value: string) {
return REGEXP_REGEXP.test(value);
},
construct(value: string): RegExp {
const match = REGEXP_REGEXP.exec(value);
if (isNil(match)) {
throw new InvalidArgumentError('invalid regexp');
}
const [/* input */, expr, flags] = Array.from(match);
return new RegExp(expr, flags);
},
});

View File

@ -1,22 +0,0 @@
import { Type as YamlType } from 'js-yaml';
import { NotFoundError } from '../../error/NotFoundError';
const ALLOWED_STREAMS = new Set([
'stdout',
'stderr',
]);
export const streamType = new YamlType('!stream', {
kind: 'scalar',
resolve(name: string) {
if (ALLOWED_STREAMS.has(name) && Reflect.has(process, name)) {
return true;
} else {
throw new NotFoundError(`process stream not found: ${name}`);
}
},
construct(name: string) {
return Reflect.get(process, name);
},
});

View File

@ -1,7 +0,0 @@
import { BaseError } from 'noicejs';
export class InvalidArgumentError extends BaseError {
constructor(msg = 'invalid argument passed', ...nested: Array<Error>) {
super(msg, ...nested);
}
}

View File

@ -1,7 +0,0 @@
import { BaseError } from 'noicejs';
export class NotFoundError extends BaseError {
constructor(msg = 'value not found', ...nested: Array<Error>) {
super(msg, ...nested);
}
}

View File

@ -1,6 +1,6 @@
import { CONFIG_SCHEMA } from '@apextoaster/js-yaml-schema';
import { safeDump, safeLoadAll } from 'js-yaml';
import { CONFIG_SCHEMA } from '../config/schema';
import { Parser } from '../parser';
/* eslint-disable @typescript-eslint/no-explicit-any */

View File

@ -1,8 +1,8 @@
import { hasItems } from '@apextoaster/js-utils';
import { applyDiff, diff } from 'deep-diff';
import { cloneDeep } from 'lodash';
import { Rule } from '.';
import { hasItems } from '../utils';
import { Visitor } from '../visitor';
import { VisitorContext } from '../visitor/VisitorContext';

View File

@ -1,9 +1,9 @@
import { doesExist, hasItems } from '@apextoaster/js-utils';
import { ErrorObject, ValidateFunction } from 'ajv';
import { cloneDeep, defaultTo, isNil } from 'lodash';
import { LogLevel } from 'noicejs';
import { Rule, RuleData } from '.';
import { doesExist, hasItems } from '../utils';
import { Visitor, VisitorError, VisitorResult } from '../visitor';
import { VisitorContext } from '../visitor/VisitorContext';

View File

@ -1,3 +1,4 @@
import { doesExist, ensureArray } from '@apextoaster/js-utils';
import { ValidateFunction } from 'ajv';
import { Dictionary, intersection } from 'lodash';
import { Minimatch } from 'minimatch';
@ -7,7 +8,6 @@ import recursive from 'recursive-readdir';
import ruleSchemaData from '../../rules/salty-dog.yml';
import { YamlParser } from '../parser/YamlParser';
import { readFile } from '../source';
import { doesExist, ensureArray } from '../utils';
import { VisitorResult } from '../visitor';
import { VisitorContext } from '../visitor/VisitorContext';
import { SchemaRule } from './SchemaRule';

View File

@ -1,5 +1,5 @@
import { isNil } from '@apextoaster/js-utils';
import { readdir, readFile as readBack, writeFile as writeBack } from 'fs';
import { isNil } from 'lodash';
import { promisify } from 'util';
export const FILE_ENCODING = 'utf-8';

View File

@ -1,29 +0,0 @@
import { isNil } from 'lodash';
/* eslint-disable @typescript-eslint/ban-types */
export function doesExist<T>(val: T | null | undefined): val is T {
return !isNil(val);
}
/**
* Test if a value is an array with some items (length > 0).
*
* This is not a general replacement for `.length > 0`, since it is also a typeguard:
* `if (hasItems(val)) else { val }` will complain that `val` is `never` in the `else`
* branch, since it was proven not to be an array by this function, even if `val` is
* simply empty.
*/
export function hasItems<T>(val: Array<T> | null | undefined): val is Array<T>;
export function hasItems<T>(val: ReadonlyArray<T> | null | undefined): val is ReadonlyArray<T>;
export function hasItems<T>(val: ReadonlyArray<T> | null | undefined): val is ReadonlyArray<T> {
return (Array.isArray(val) && val.length > 0);
}
export function ensureArray<T>(val: Array<T> | undefined): Array<T> {
if (isNil(val)) {
return [];
} else {
return Array.from(val);
}
}

View File

@ -1,9 +1,9 @@
import { doesExist, hasItems } from '@apextoaster/js-utils';
import Ajv from 'ajv';
import { JSONPath } from 'jsonpath-plus';
import { Logger } from 'noicejs';
import { VisitorError, VisitorResult } from '.';
import { doesExist, hasItems } from '../utils';
/* eslint-disable @typescript-eslint/no-explicit-any */

View File

@ -1,8 +1,8 @@
import { NotFoundError } from '@apextoaster/js-utils';
import { expect } from 'chai';
import { join } from 'path';
import { loadConfig, readConfig } from '../../src/config';
import { NotFoundError } from '../../src/error/NotFoundError';
import { describeLeaks, itLeaks } from '../helpers/async';
describeLeaks('load config helper', async () => {

View File

@ -1,22 +0,0 @@
import { expect } from 'chai';
import { envType } from '../../../src/config/type/Env';
import { NotFoundError } from '../../../src/error/NotFoundError';
import { VERSION_INFO } from '../../../src/version';
import { describeLeaks, itLeaks } from '../../helpers/async';
describeLeaks('env config type', async () => {
itLeaks('should throw on missing variables', async () => {
expect(() => {
envType.resolve('DOES_NOT_EXIST_');
}).to.throw(NotFoundError);
});
itLeaks('should resolve existing variables', async () => {
expect(envType.resolve('CI_COMMIT_SHA')).to.equal(true);
});
itLeaks('should construct a value from variables', async () => {
expect(envType.construct('CI_COMMIT_SHA')).to.equal(VERSION_INFO.git.commit);
});
});

View File

@ -1,48 +0,0 @@
import { expect } from 'chai';
import { BaseError } from 'noicejs';
import { join } from 'path';
import { includeType, resolvePath } from '../../../src/config/type/Include';
import { NotFoundError } from '../../../src/error/NotFoundError';
import { describeLeaks, itLeaks } from '../../helpers/async';
const TEST_ROOT = '../test/config/type';
const CONFIG_MISSING = 'missing.yml';
describeLeaks('include config type', async () => {
itLeaks('should resolve existing files', async () => {
expect(includeType.resolve(join(TEST_ROOT, 'include.yml'))).to.equal(true);
});
itLeaks('should throw when resolving missing files', async () => {
expect(() => {
includeType.resolve(join(TEST_ROOT, CONFIG_MISSING));
}).to.throw(NotFoundError);
});
itLeaks('should construct data from file', async () => {
expect(includeType.construct(join(TEST_ROOT, 'include.yml'))).to.equal('test');
});
itLeaks('should throw when constructing missing files', async () => {
expect(() => {
includeType.construct(join(TEST_ROOT, CONFIG_MISSING));
}).to.throw(BaseError);
});
itLeaks('should throw when resolving missing files', async () => {
expect(() => {
includeType.resolve(join(TEST_ROOT, CONFIG_MISSING));
}).to.throw(BaseError);
});
});
describeLeaks('resolve path helper', async () => {
itLeaks('should resolve relative paths relative to dirname', async () => {
expect(resolvePath('./index.js')).to.equal(join(__dirname, 'index.js'));
});
itLeaks('should resolve absolute paths to themselves', async () => {
expect(resolvePath('/')).to.equal('/');
});
});

View File

@ -1,32 +0,0 @@
import { expect } from 'chai';
import { regexpType } from '../../../src/config/type/Regexp';
import { InvalidArgumentError } from '../../../src/error/InvalidArgumentError';
import { describeLeaks, itLeaks } from '../../helpers/async';
describeLeaks('regexp config type', async () => {
itLeaks('match slashed strings', async () => {
expect(regexpType.resolve('/foo/')).to.equal(true);
});
itLeaks('should match flags', async () => {
const regexp: RegExp = regexpType.construct('/foo/g');
expect(regexp.flags).to.equal('g');
});
itLeaks('should not match bare strings', async () => {
expect(regexpType.resolve('foo')).to.equal(false);
});
itLeaks('should not match invalid flags', async () => {
expect(regexpType.resolve('/foo/notrealflags')).to.equal(false);
});
itLeaks('should not match regexp embedded in a longer string', async () => {
expect(regexpType.resolve('some/regexp/with-padding')).to.equal(false);
});
itLeaks('should throw when constructing an invalid regexp', async () => {
expect(() => regexpType.construct('/foo/notrealflags')).to.throw(InvalidArgumentError);
});
});

View File

@ -1,38 +0,0 @@
import { expect } from 'chai';
import { streamType } from '../../../src/config/type/Stream';
import { NotFoundError } from '../../../src/error/NotFoundError';
import { describeLeaks, itLeaks } from '../../helpers/async';
const TEST_STREAMS = [{
name: 'stderr',
stream: process.stderr,
}, {
name: 'stdin',
stream: process.stdin,
}, {
name: 'stdout',
stream: process.stdout,
}];
describeLeaks('stream config type', async () => {
itLeaks('should resolve allowed streams', async () => {
expect(streamType.resolve('stderr')).to.equal(true);
expect(streamType.resolve('stdout')).to.equal(true);
});
itLeaks('should throw when stream is not allowed', async () => {
expect(() => streamType.resolve('stdin')).to.throw(NotFoundError);
});
itLeaks('should throw when stream does not exist', async () => {
expect(() => streamType.resolve('imaginary')).to.throw(NotFoundError);
});
itLeaks('should construct streams', async () => {
for (const {name, stream} of TEST_STREAMS) {
expect(streamType.construct(name)).to.equal(stream, `should construct stream ${name}`);
}
});
});

View File

@ -1 +0,0 @@
test

View File

@ -1,34 +0,0 @@
import { expect } from 'chai';
import { kebabCase } from 'lodash';
import { InvalidArgumentError } from '../../src/error/InvalidArgumentError';
import { NotFoundError } from '../../src/error/NotFoundError';
const errors = [
InvalidArgumentError,
NotFoundError,
];
describe('errors', () => {
for (const errorType of errors) {
describe(kebabCase(errorType.name), () => {
it('should have a message', () => {
const err = new errorType();
expect(err.message).to.not.equal('');
});
it('should include nested errors in the stack trace', () => {
const inner = new Error('inner error');
const err = new errorType('outer error', inner);
expect(err.stack).to.include('inner', 'inner error message').and.include('outer', 'outer error message');
});
it('should have the nested error', () => {
const inner = new Error('inner error');
const err = new errorType('outer error', inner);
expect(err.cause()).to.equal(inner);
expect(err.length).to.equal(1);
});
});
}
});

View File

@ -1,4 +1,4 @@
import { AsyncHook, createHook } from 'async_hooks';
import { AsyncTracker, isDebug, isNil } from '@apextoaster/js-utils';
// this will pull Mocha internals out of the stacks
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
@ -8,97 +8,13 @@ const filterStack = stackTraceFilter();
type AsyncMochaTest = (this: Mocha.Context | void) => Promise<void>;
type AsyncMochaSuite = (this: Mocha.Suite) => Promise<void>;
/* eslint-disable-next-line @typescript-eslint/ban-types */
function isNil<T>(val: T | null | undefined): val is null | undefined {
/* eslint-disable-next-line no-null/no-null */
return val === null || val === undefined;
}
export interface TrackedResource {
source: string;
triggerAsyncId: number;
type: string;
}
function debugMode() {
return Reflect.has(process.env, 'DEBUG');
}
/**
* Async resource tracker using node's internal hooks.
*
* This probably won't work in a browser. It does not hold references to the resource, to avoid leaks.
* Adapted from https://gist.github.com/boneskull/7fe75b63d613fa940db7ec990a5f5843#file-async-dump-js
*/
export class Tracker {
public static getStack(): string {
const err = new Error();
if (isNil(err.stack)) {
return 'no stack trace available';
} else {
return filterStack(err.stack);
}
}
private readonly hook: AsyncHook;
private readonly resources: Map<number, TrackedResource>;
constructor() {
this.resources = new Map();
this.hook = createHook({
destroy: (id: number) => {
this.resources.delete(id);
},
init: (id: number, type: string, triggerAsyncId: number) => {
const source = Tracker.getStack();
// @TODO: exclude async hooks, including this one
this.resources.set(id, {
source,
triggerAsyncId,
type,
});
},
promiseResolve: (id: number) => {
this.resources.delete(id);
},
});
}
public clear() {
this.resources.clear();
}
public disable() {
this.hook.disable();
}
/* eslint-disable no-console, no-invalid-this */
public dump() {
console.error(`tracking ${this.resources.size} async resources`);
this.resources.forEach((res, id) => {
console.error(`${id}: ${res.type}`);
if (debugMode()) {
console.error(res.source);
console.error('\n');
}
});
}
public enable() {
this.hook.enable();
}
public get size(): number {
return this.resources.size;
}
}
/**
* Describe a suite of async tests. This wraps mocha's describe to track async resources and report leaks.
*/
export function describeLeaks(description: string, cb: AsyncMochaSuite): Mocha.Suite {
return describe(description, function trackSuite(this: Mocha.Suite) {
const tracker = new Tracker();
const tracker = new AsyncTracker();
tracker.filter = filterStack;
beforeEach(() => {
tracker.enable();
@ -112,7 +28,7 @@ export function describeLeaks(description: string, cb: AsyncMochaSuite): Mocha.S
if (leaked > 1) {
tracker.dump();
const msg = `test leaked ${leaked - 1} async resources`;
if (debugMode()) {
if (isDebug()) {
throw new Error(msg);
} else {
/* eslint-disable-next-line no-console */
@ -123,6 +39,7 @@ export function describeLeaks(description: string, cb: AsyncMochaSuite): Mocha.S
tracker.clear();
});
/* eslint-disable-next-line no-invalid-this */
const suite: PromiseLike<void> | undefined = cb.call(this);
if (isNil(suite) || !Reflect.has(suite, 'then')) {
/* eslint-disable-next-line no-console */
@ -145,6 +62,7 @@ export function itLeaks(expectation: string, cb?: AsyncMochaTest): Mocha.Test {
return it(expectation, function trackTest(this: Mocha.Context) {
return new Promise<unknown>((res, rej) => {
/* eslint-disable-next-line no-invalid-this */
cb.call(this).then((value: unknown) => {
res(value);
}, (err: Error) => {

View File

@ -2,6 +2,16 @@
# yarn lockfile v1
"@apextoaster/js-utils@^0.1.7":
version "0.1.7"
resolved "https://artifacts.apextoaster.com/repository/group-npm/@apextoaster/js-utils/-/js-utils-0.1.7.tgz#0abab0b23a0d94ff96405b46bf5907844685722e"
integrity sha512-NB1n8y6KjKev8+WZNcYVNCi0FYEA6RVHcNjxTki/gQklZNayKPkGP7WtkVF2IuvEo7L6N0sYkWwxRsl+oQWulw==
"@apextoaster/js-yaml-schema@^0.2.0":
version "0.2.0"
resolved "https://artifacts.apextoaster.com/repository/group-npm/@apextoaster/js-yaml-schema/-/js-yaml-schema-0.2.0.tgz#af69f7782ceb7b909e598f0a360fdada06d6693d"
integrity sha512-qIqIb6tsY0acv/LZqkWNyB1kooq2mwf7AS3Acs568zvSG6bN82UwBXf8r3yJh6F/0Yjeqp/UZyxTvNPsLizh/w==
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
version "7.5.5"
resolved "https://artifacts.apextoaster.com/repository/group-npm/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"