-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(shortstop): bundle more shortstop handlers, add tests, release
- Loading branch information
Showing
14 changed files
with
310 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ name: Node.js Package | |
on: | ||
push: | ||
branches: | ||
- main-notyet | ||
- main | ||
|
||
jobs: | ||
build: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
root: | ||
this-yaml: is-bad | ||
- why: because |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
root: | ||
key: value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { afterAll, beforeEach, describe, expect, test } from 'vitest'; | ||
|
||
import { envHandler } from './envHandler'; | ||
|
||
describe('env', () => { | ||
const originalEnv = process.env; | ||
let handler: ReturnType<typeof envHandler>; | ||
|
||
beforeEach(() => { | ||
process.env = { ...process.env, SAMPLE: '8000' }; | ||
handler = envHandler(); | ||
}); | ||
|
||
afterAll(() => { | ||
process.env = originalEnv; | ||
}); | ||
|
||
test('should validate handler is a function with length 1', () => { | ||
expect(typeof handler).toBe('function'); | ||
expect(handler.length).toBe(1); | ||
}); | ||
|
||
test('should validate raw env values', () => { | ||
expect(handler('SAMPLE')).toBe(process.env.SAMPLE); | ||
}); | ||
|
||
test('should validate env values as numbers', () => { | ||
expect(handler('SAMPLE|d')).toBe(8000); | ||
}); | ||
|
||
test('should validate NaN env values', () => { | ||
process.env.SAMPLE = ''; | ||
expect(isNaN(handler('SAMPLE|d') as number)).toBe(true); | ||
|
||
process.env.SAMPLE = 'hello'; | ||
expect(isNaN(handler('SAMPLE|d') as number)).toBe(true); | ||
}); | ||
|
||
test('should validate boolean env values', () => { | ||
process.env.SAMPLE = '8000'; | ||
expect(handler('SAMPLE|b')).toBe(true); | ||
|
||
process.env.SAMPLE = 'true'; | ||
expect(handler('SAMPLE|b')).toBe(true); | ||
|
||
process.env.SAMPLE = 'false'; | ||
expect(handler('SAMPLE|b')).toBe(false); | ||
|
||
process.env.SAMPLE = '0'; | ||
expect(handler('SAMPLE|b')).toBe(false); | ||
|
||
delete process.env.SAMPLE; | ||
expect(handler('SAMPLE|b')).toBe(false); | ||
}); | ||
|
||
test('should validate boolean inverse env values', () => { | ||
process.env.SAMPLE = '8000'; | ||
expect(handler('SAMPLE|!b')).toBe(false); | ||
|
||
process.env.SAMPLE = 'true'; | ||
expect(handler('SAMPLE|!b')).toBe(false); | ||
|
||
process.env.SAMPLE = 'false'; | ||
expect(handler('SAMPLE|!b')).toBe(true); | ||
|
||
process.env.SAMPLE = '0'; | ||
expect(handler('SAMPLE|!b')).toBe(true); | ||
|
||
delete process.env.SAMPLE; | ||
expect(handler('SAMPLE|!b')).toBe(true); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
export function envHandler() { | ||
const filters: { | ||
[key: string]: (value?: string) => number | boolean; | ||
} = { | ||
'|d': (value?: string) => { | ||
return parseInt(value || '', 10); | ||
}, | ||
'|b': (value?: string) => { | ||
return value !== '' && value !== 'false' && value !== '0' && value !== undefined; | ||
}, | ||
'|!b': (value?: string) => { | ||
return value === '' || value === 'false' || value === '0' || value === undefined; | ||
}, | ||
}; | ||
|
||
return function envHandler(value: string): string | number | boolean | undefined { | ||
let result: string | number | boolean | undefined = process.env[value]; | ||
|
||
Object.entries(filters).some(([key, fn]) => { | ||
if (value.endsWith(key)) { | ||
const sliced = value.slice(0, -key.length); | ||
result = fn(process.env[sliced]); | ||
return true; | ||
} | ||
return false; | ||
}); | ||
|
||
return result; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import Path from 'path'; | ||
|
||
import { describe, expect, test } from 'vitest'; | ||
|
||
import { globHandler, pathHandler, yamlHandler } from './fileHandlers'; | ||
|
||
describe('file related shortstop handlers', () => { | ||
test('path shortstop handler', () => { | ||
expect(typeof pathHandler).toBe('function'); | ||
expect(pathHandler.length).toBe(1); | ||
|
||
const handler = pathHandler(); | ||
// Default dirname | ||
expect(typeof handler).toBe('function'); | ||
expect(handler.length).toBe(1); | ||
|
||
// Absolute path | ||
expect(handler(__filename)).toBe(__filename); | ||
|
||
// Relative Path | ||
expect(handler(Path.basename(__filename))).toBe(__filename); | ||
|
||
const specHandler = pathHandler(__dirname); | ||
expect(typeof specHandler).toBe('function'); | ||
expect(specHandler.length).toBe(1); | ||
|
||
// Absolute path | ||
expect(specHandler(__filename)).toBe(__filename); | ||
|
||
// Relative Path | ||
expect(specHandler(Path.basename(__filename))).toBe(__filename); | ||
}); | ||
|
||
test('yaml shortstop handler', async () => { | ||
const handler = yamlHandler(Path.join(__dirname, '..', '..', '__tests__', 'yaml')); | ||
|
||
expect(() => handler('good.yaml')).not.toThrow(); | ||
expect(() => handler('bad.yaml')).rejects.toThrow(); | ||
expect(handler('good.yaml')).resolves.toMatchInlineSnapshot(` | ||
{ | ||
"root": { | ||
"key": "value", | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
test('glob shortstop handler', async () => { | ||
const basedir = Path.join(__dirname, '..', '..', '__tests__', 'yaml'); | ||
const handler = globHandler(basedir); | ||
expect(typeof handler).toBe('function'); | ||
expect(handler.length).toBe(1); | ||
|
||
let matches = await handler('**/*.js'); | ||
expect(matches?.length).toBe(0); | ||
matches = await handler('**/*.yaml'); | ||
expect(matches?.length).toBe(2); | ||
matches = await handler('bad*'); | ||
expect(matches?.length).toBe(1); | ||
expect(matches[0]).toBe(Path.join(basedir, 'bad.yaml')); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import fs from 'fs/promises'; | ||
import Path from 'path'; | ||
|
||
import { glob, type GlobOptions } from 'glob'; | ||
import yaml from 'js-yaml'; | ||
import caller from 'caller'; | ||
|
||
(function expandModulePaths() { | ||
// If this module is deployed outside the app's node_modules, it wouldn't be | ||
// able to resolve other modules deployed under app while evaluating this shortstops. | ||
// Adding app's node_modules folder to the paths will help handle this case. | ||
const paths = module.paths || []; | ||
const appNodeModules = Path.resolve(process.cwd(), 'node_modules'); | ||
if (paths.indexOf(appNodeModules) < 0) { | ||
// Assuming Module._nodeModulePaths creates a new module.paths object for each module. | ||
paths.push(appNodeModules); | ||
} | ||
})(); | ||
|
||
/** | ||
* Return an absolute path for the given value. | ||
*/ | ||
export function pathHandler(basedir?: string) { | ||
const basedirOrDefault = basedir || Path.dirname(caller()); | ||
return function pathHandler(value: string) { | ||
if (Path.resolve(value) === value) { | ||
// Absolute path already, so just return it. | ||
return value; | ||
} | ||
const components = value.split('/'); | ||
components.unshift(basedirOrDefault); | ||
return Path.resolve(...components); | ||
}; | ||
} | ||
|
||
type ReadOptions = Parameters<typeof fs.readFile>[1]; | ||
|
||
/** | ||
* Return the contents of a file. | ||
*/ | ||
export function fileHandler(basedir?: string | ReadOptions, options?: ReadOptions) { | ||
const finalBasedir = typeof basedir === 'string' ? basedir : undefined; | ||
const finalOptions = typeof basedir === 'object' ? basedir : options; | ||
|
||
const pathhandler = pathHandler(finalBasedir); | ||
return async function fileHandler(value: string) { | ||
return fs.readFile( | ||
pathhandler(value), | ||
finalOptions || { | ||
encoding: null, | ||
flag: 'r', | ||
}, | ||
); | ||
}; | ||
} | ||
|
||
/** | ||
* Call require() on a module and return the loaded module | ||
*/ | ||
export function requireHandler(basedir?: string): ReturnType<typeof require> { | ||
const resolvePath = pathHandler(basedir); | ||
return function requireHandler(value: string) { | ||
let module = value; | ||
// @see http://nodejs.org/api/modules.html#modules_file_modules | ||
if (value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) { | ||
// NOTE: Technically, paths with a leading '/' don't need to be resolved, but | ||
// leaving for consistency. | ||
module = resolvePath(module); | ||
} | ||
|
||
return require(module); | ||
}; | ||
} | ||
|
||
/** | ||
* Load a YAML file and return the parsed content. | ||
*/ | ||
export function yamlHandler(basedir?: string, options: yaml.LoadOptions = {}) { | ||
const resolver = pathHandler(basedir); | ||
|
||
return async function yamlParser(value: string) { | ||
const filename = resolver(value); | ||
|
||
const content = await fs.readFile(filename, 'utf8'); | ||
return yaml.load(content, { ...options, filename }); | ||
}; | ||
} | ||
|
||
export function execHandler(basedir?: string) { | ||
const resolver = requireHandler(basedir); | ||
|
||
return function execHandler(value: string) { | ||
const tuple = value.split('#'); | ||
const module = resolver(tuple[0]); | ||
const method = module[tuple[1]] || module; | ||
|
||
if (typeof method !== 'function') { | ||
throw new Error(`exec: unable to find method ${tuple[1] || 'default'} on module ${tuple[0]}`); | ||
} | ||
|
||
return method(); | ||
}; | ||
} | ||
|
||
export function globHandler(optionsOrCwd?: GlobOptions | string) { | ||
const options = typeof optionsOrCwd === 'string' ? { cwd: optionsOrCwd } : (optionsOrCwd || {}); | ||
options.cwd = options.cwd || Path.dirname(caller()); | ||
|
||
const resolvePath = pathHandler(options.cwd.toString()); | ||
|
||
return async function globHandler(value: string) { | ||
const matches = await glob(value, options); | ||
return matches.map((relativePath) => resolvePath(relativePath.toString())); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
export * from './create'; | ||
export * from './fileHandlers'; | ||
export * from './textHandlers'; | ||
export * from './envHandler'; |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export function base64Handler() { | ||
return function base64Handler(value: string) { | ||
return Buffer.from(value, 'base64'); | ||
}; | ||
} |
Oops, something went wrong.