fix: refactor duplicate utils into js-utils, export symbols
BREAKING CHANGE: extract typed errors and use the equivalent from js-utils
This commit is contained in:
parent
bbd354d2e2
commit
859baa33f9
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [@apextoaster/js-yaml-schema](./js-yaml-schema.md) > [CONFIG\_SCHEMA](./js-yaml-schema.config_schema.md)
|
||||
|
||||
## CONFIG\_SCHEMA variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
CONFIG_SCHEMA: Schema
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [@apextoaster/js-yaml-schema](./js-yaml-schema.md) > [envType](./js-yaml-schema.envtype.md)
|
||||
|
||||
## envType variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
envType: YamlType
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [@apextoaster/js-yaml-schema](./js-yaml-schema.md) > [includeSchema](./js-yaml-schema.includeschema.md)
|
||||
|
||||
## includeSchema variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
includeSchema: {
|
||||
schema: import("js-yaml").Schema;
|
||||
}
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [@apextoaster/js-yaml-schema](./js-yaml-schema.md) > [includeType](./js-yaml-schema.includetype.md)
|
||||
|
||||
## includeType variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
includeType: YamlType
|
||||
```
|
|
@ -4,3 +4,14 @@
|
|||
|
||||
## js-yaml-schema package
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| [CONFIG\_SCHEMA](./js-yaml-schema.config_schema.md) | |
|
||||
| [envType](./js-yaml-schema.envtype.md) | |
|
||||
| [includeSchema](./js-yaml-schema.includeschema.md) | |
|
||||
| [includeType](./js-yaml-schema.includetype.md) | |
|
||||
| [regexpType](./js-yaml-schema.regexptype.md) | |
|
||||
| [streamType](./js-yaml-schema.streamtype.md) | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [@apextoaster/js-yaml-schema](./js-yaml-schema.md) > [regexpType](./js-yaml-schema.regexptype.md)
|
||||
|
||||
## regexpType variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
regexpType: YamlType
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [@apextoaster/js-yaml-schema](./js-yaml-schema.md) > [streamType](./js-yaml-schema.streamtype.md)
|
||||
|
||||
## streamType variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
streamType: YamlType
|
||||
```
|
|
@ -11,6 +11,7 @@
|
|||
"author": "ssube",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@apextoaster/js-utils": "^0.1.1",
|
||||
"@istanbuljs/nyc-config-typescript": "1.0.1",
|
||||
"@microsoft/api-documenter": "7.7.15",
|
||||
"@microsoft/api-extractor": "7.7.10",
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { BaseError } from 'noicejs';
|
||||
|
||||
export class InvalidArgumentError extends BaseError {
|
||||
constructor(msg = 'invalid argument passed', ...nested: Array<Error>) {
|
||||
super(msg, ...nested);
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { BaseError } from 'noicejs';
|
||||
|
||||
export class NotFoundError extends BaseError {
|
||||
constructor(msg = 'value not found', ...nested: Array<Error>) {
|
||||
super(msg, ...nested);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
import { main } from './app';
|
||||
|
||||
export { CONFIG_SCHEMA } from './schema';
|
||||
export { envType } from './type/Env';
|
||||
export { includeSchema, includeType } from './type/Include';
|
||||
export { regexpType } from './type/Regexp';
|
||||
export { streamType } from './type/Stream';
|
||||
|
||||
const STATUS_ERROR = 1;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { DEFAULT_SAFE_SCHEMA, Schema } from 'js-yaml';
|
||||
|
||||
import { envType } from './type/Env';
|
||||
import { includeType } from './type/Include';
|
||||
import { includeSchema, includeType } from './type/Include';
|
||||
import { regexpType } from './type/Regexp';
|
||||
import { streamType } from './type/Stream';
|
||||
|
||||
|
@ -11,3 +11,5 @@ export const CONFIG_SCHEMA = Schema.create([DEFAULT_SAFE_SCHEMA], [
|
|||
regexpType,
|
||||
streamType,
|
||||
]);
|
||||
|
||||
includeSchema.schema = CONFIG_SCHEMA;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { NotFoundError } from '@apextoaster/js-utils';
|
||||
import { Type as YamlType } from 'js-yaml';
|
||||
|
||||
import { NotFoundError } from '../error/NotFoundError';
|
||||
|
||||
export const envType = new YamlType('!env', {
|
||||
kind: 'scalar',
|
||||
resolve(name: string) {
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { InvalidArgumentError, NotFoundError } from '@apextoaster/js-utils';
|
||||
import { existsSync, readFileSync, realpathSync } from 'fs';
|
||||
import { SAFE_SCHEMA, safeLoad, Type as YamlType } from 'js-yaml';
|
||||
import { join } from 'path';
|
||||
|
||||
import { InvalidArgumentError } from '../error/InvalidArgumentError';
|
||||
import { NotFoundError } from '../error/NotFoundError';
|
||||
|
||||
// work around the circular dependency by setting the schema later
|
||||
export const includeSchema = {
|
||||
schema: SAFE_SCHEMA,
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { InvalidArgumentError } from '@apextoaster/js-utils';
|
||||
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', {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { NotFoundError } from '@apextoaster/js-utils';
|
||||
import { Type as YamlType } from 'js-yaml';
|
||||
|
||||
import { NotFoundError } from '../error/NotFoundError';
|
||||
|
||||
const ALLOWED_STREAMS = new Set([
|
||||
'stdout',
|
||||
'stderr',
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
import { AsyncHook, createHook } from 'async_hooks';
|
||||
|
||||
// this will pull Mocha internals out of the stacks
|
||||
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
|
||||
const { stackTraceFilter } = require('mocha/lib/utils');
|
||||
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();
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.enable();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.disable();
|
||||
const leaked = tracker.size;
|
||||
|
||||
// @TODO: this should only exclude the single Immediate set by the Tracker
|
||||
if (leaked > 1) {
|
||||
tracker.dump();
|
||||
const msg = `test leaked ${leaked - 1} async resources`;
|
||||
if (debugMode()) {
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.warn(msg);
|
||||
}
|
||||
}
|
||||
|
||||
tracker.clear();
|
||||
});
|
||||
|
||||
const suite: PromiseLike<void> | undefined = cb.call(this);
|
||||
if (isNil(suite) || !Reflect.has(suite, 'then')) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(`test suite '${description}' did not return a promise`);
|
||||
}
|
||||
|
||||
return suite;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an asynchronous test with unhandled rejection guards.
|
||||
*
|
||||
* This function may not have any direct test coverage. It is too simple to reasonably mock.
|
||||
*/
|
||||
export function itLeaks(expectation: string, cb?: AsyncMochaTest): Mocha.Test {
|
||||
if (isNil(cb)) {
|
||||
return it(expectation);
|
||||
}
|
||||
|
||||
return it(expectation, function trackTest(this: Mocha.Context) {
|
||||
return new Promise<unknown>((res, rej) => {
|
||||
cb.call(this).then((value: unknown) => {
|
||||
res(value);
|
||||
}, (err: Error) => {
|
||||
rej(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,22 +1,21 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { NotFoundError } from '../../src/error/NotFoundError';
|
||||
import { NotFoundError } from '@apextoaster/js-utils';
|
||||
import { envType } from '../../src/type/Env';
|
||||
import { VERSION_INFO } from '../../src/version';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
describeLeaks('env config type', async () => {
|
||||
itLeaks('should throw on missing variables', async () => {
|
||||
describe('env config type', async () => {
|
||||
it('should throw on missing variables', async () => {
|
||||
expect(() => {
|
||||
envType.resolve('DOES_NOT_EXIST_');
|
||||
}).to.throw(NotFoundError);
|
||||
});
|
||||
|
||||
itLeaks('should resolve existing variables', async () => {
|
||||
it('should resolve existing variables', async () => {
|
||||
expect(envType.resolve('CI_COMMIT_SHA')).to.equal(true);
|
||||
});
|
||||
|
||||
itLeaks('should construct a value from variables', async () => {
|
||||
it('should construct a value from variables', async () => {
|
||||
expect(envType.construct('CI_COMMIT_SHA')).to.equal(VERSION_INFO.git.commit);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
import { expect } from 'chai';
|
||||
import { join } from 'path';
|
||||
|
||||
import { InvalidArgumentError } from '../../src/error/InvalidArgumentError';
|
||||
import { NotFoundError } from '../../src/error/NotFoundError';
|
||||
import { InvalidArgumentError, NotFoundError } from '@apextoaster/js-utils';
|
||||
import { includeType } from '../../src/type/Include';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
const TEST_ROOT = '../test/type';
|
||||
|
||||
describeLeaks('include config type', async () => {
|
||||
itLeaks('should resolve existing files', async () => {
|
||||
describe('include config type', async () => {
|
||||
it('should resolve existing files', async () => {
|
||||
expect(includeType.resolve(join(TEST_ROOT, 'include.yml'))).to.equal(true);
|
||||
});
|
||||
|
||||
itLeaks('should throw when resolving missing files', async () => {
|
||||
it('should throw when resolving missing files', async () => {
|
||||
expect(() => {
|
||||
includeType.resolve(join(TEST_ROOT, 'missing.yml'));
|
||||
}).to.throw(NotFoundError);
|
||||
});
|
||||
|
||||
itLeaks('should construct data from file', async () => {
|
||||
it('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 () => {
|
||||
it('should throw when constructing missing files', async () => {
|
||||
expect(() => {
|
||||
includeType.construct(join(TEST_ROOT, 'missing.yml'));
|
||||
}).to.throw(InvalidArgumentError);
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { regexpType } from '../../src/type/Regexp';
|
||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||
|
||||
describeLeaks('regexp config type', async () => {
|
||||
itLeaks('match slashed strings', async () => {
|
||||
describe('regexp config type', async () => {
|
||||
it('match slashed strings', async () => {
|
||||
expect(regexpType.resolve('/foo/')).to.equal(true);
|
||||
});
|
||||
|
||||
itLeaks('should match flags', async () => {
|
||||
it('should match flags', async () => {
|
||||
const regexp: RegExp = regexpType.construct('/foo/g');
|
||||
expect(regexp.flags).to.equal('g');
|
||||
});
|
||||
|
||||
itLeaks('should not match bare strings', async () => {
|
||||
it('should not match bare strings', async () => {
|
||||
expect(regexpType.resolve('foo')).to.equal(false);
|
||||
});
|
||||
|
||||
itLeaks('should not match invalid flags', async () => {
|
||||
it('should not match invalid flags', async () => {
|
||||
expect(regexpType.resolve('/foo/notrealflags')).to.equal(false);
|
||||
});
|
||||
|
||||
itLeaks('should not match regex embedded in a longer string', async () => {
|
||||
it('should not match regex embedded in a longer string', async () => {
|
||||
expect(regexpType.resolve('some/regex/with-padding')).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@apextoaster/js-utils@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://artifacts.apextoaster.com/repository/group-npm/@apextoaster/js-utils/-/js-utils-0.1.1.tgz#035548ce76f12c1de4b9c20f9b9aeebd0c840546"
|
||||
integrity sha512-hJZY3smbpTPdKidZIaa+VDROkCIjaEahhPqqF7YCqvFP5E/CNbwbiNqASxZjz97Dde2Dw6II8YGDV5Ef8yfO3g==
|
||||
|
||||
"@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"
|
||||
|
|
Loading…
Reference in New Issue