@cloudpss/template
Version:
Lightweight string and object templating utilities with interpolation and formula support.
270 lines (263 loc) • 10.8 kB
text/typescript
import { randomBytes, randomFillSync } from 'node:crypto';
import { template, createVmContext } from '@cloudpss/template';
describe('template', () => {
it('should handle primitives', () => {
expect(template('')()).toEqual('');
expect(template('Hello, world!')()).toEqual('Hello, world!');
expect(template(1)()).toEqual(1);
expect(template(1.1)()).toEqual(1.1);
expect(template(Number.NaN)()).toEqual(Number.NaN);
expect(template(Number.POSITIVE_INFINITY)()).toEqual(Number.POSITIVE_INFINITY);
expect(template(Number.NEGATIVE_INFINITY)()).toEqual(Number.NEGATIVE_INFINITY);
expect(template(Number.EPSILON)()).toEqual(Number.EPSILON);
expect(template(true)()).toEqual(true);
expect(template(false)()).toEqual(false);
expect(template(null)()).toEqual(null);
expect(template(undefined)()).toEqual(undefined);
expect(template(1n)()).toEqual(1n);
expect(
template({
string: 'Hello, world!',
number: 1,
boolean: true,
null: null,
undefined: undefined,
bigint: 1n,
})(),
).toEqual({
string: 'Hello, world!',
number: 1,
boolean: true,
null: null,
undefined: undefined,
bigint: 1n,
});
});
it('should ignore symbols', () => {
expect(template(Symbol(''))()).toEqual(undefined);
expect(
template({
key: Symbol(''),
[Symbol('')]: 1,
})(),
).toEqual({
key: undefined,
});
});
it('should ignore functions', () => {
expect(template(() => 1)()).toEqual(undefined);
expect(
template({
key: () => 1,
})(),
).toEqual({
key: undefined,
});
});
it('should copy arrays', () => {
const arr = [1, 2, {}];
const result = template(arr)();
expect(result).toEqual(arr);
expect(result).not.toBe(arr);
expect(result[2]).not.toBe(arr[2]);
});
it('should copy objects', () => {
const obj = { a: 1, b: 2, c: {} };
const result = template(obj)();
expect(result).toEqual(obj);
expect(result).not.toBe(obj);
expect(result.c).not.toBe(obj.c);
});
it('should copy dates', () => {
const date = new Date();
const result = template(date)();
expect(result).toEqual(date);
expect(result).not.toBe(date);
});
it('should copy regexps', () => {
const regexp = /./gi;
const result = template(regexp)();
expect(result).toEqual(regexp);
expect(result).not.toBe(regexp);
});
describe('should copy errors', () => {
it('Error', () => {
const error = new Error('test error');
const result = template(error)();
expect(result).toBeInstanceOf(Error);
expect(result.name).toBe(error.name);
expect(result.message).toBe(error.message);
expect(result).not.toBe(error);
});
it('TypeError', () => {
const error = new TypeError('test error');
const result = template(error)();
expect(result).toBeInstanceOf(TypeError);
expect(result.name).toBe(error.name);
expect(result.message).toBe(error.message);
expect(result).not.toBe(error);
});
it('SyntaxError with custom name', () => {
const error = new SyntaxError('test error');
error.name = 'CustomError';
const result = template(error)();
expect(result).toBeInstanceOf(SyntaxError);
expect(result.name).toBe(error.name);
expect(result.message).toBe(error.message);
expect(result).not.toBe(error);
});
it('DOMException', () => {
const error = new DOMException('test error', 'AbortError');
const result = template(error)();
expect(result).toBeInstanceOf(DOMException);
expect(result.name).toBe(error.name);
expect(result.message).toBe(error.message);
expect(result.code).toBe(error.code);
expect(result).not.toBe(error);
});
it('CustomError', () => {
const error = new (class CustomError extends Error {})('test error');
const result = template(error)();
expect(result).toBeInstanceOf(Error);
expect(result.name).toBe(error.name);
expect(result.message).toBe(error.message);
expect(result).not.toBe(error);
});
});
describe('should copy ArrayBuffers', () => {
it('ArrayBuffer', () => {
const { buffer } = randomBytes(16);
const t = template(buffer);
const result1 = t();
const result2 = t();
expect(new Uint8Array(result1)).toEqual(new Uint8Array(buffer));
expect(new Uint8Array(result2)).toEqual(new Uint8Array(buffer));
expect(result1).not.toBe(buffer);
expect(result2).not.toBe(buffer);
expect(result1).not.toBe(result2);
});
it('SharedArrayBuffer', () => {
const buffer = new SharedArrayBuffer(16);
randomFillSync(new Uint8Array(buffer));
const result = template(buffer)();
expect(new Uint8Array(result)).toEqual(new Uint8Array(buffer));
expect(result).toBeInstanceOf(ArrayBuffer);
expect(result).not.toBe(buffer);
});
});
describe('should copy ArrayBufferViews', () => {
it('DataView', () => {
const { buffer } = randomBytes(64);
const view = new DataView(buffer, 32, 16);
const result = template(view)();
expect(result).toEqual(view);
expect(result).not.toBe(view);
expect(new Uint8Array(result.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
expect(result.buffer).not.toBe(buffer);
});
it('Uint8Array', () => {
const { buffer } = randomBytes(64);
const view = new Uint8Array(buffer, 32, 16);
const result = template(view)();
expect(result).toEqual(view);
expect(result).not.toBe(view);
expect(new Uint8Array(result.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
expect(result.buffer).not.toBe(buffer);
});
it('Float64Array', () => {
const { buffer } = randomBytes(64);
const view = new Float64Array(buffer, 32, 1);
const result = template(view)();
expect(result).toEqual(view);
expect(result).not.toBe(view);
expect(new Uint8Array(result.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
expect(result.buffer).not.toBe(buffer);
});
it('BigInt64Array', () => {
const { buffer } = randomBytes(64);
const view = new BigInt64Array(buffer, 32, 1);
const t = template(view);
const result1 = t();
const result2 = t();
expect(result1).toEqual(view);
expect(result1).not.toBe(view);
expect(result2).toEqual(view);
expect(result2).not.toBe(view);
expect(result2).not.toBe(result1);
expect(new Uint8Array(result1.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
expect(new Uint8Array(result2.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
expect(result1.buffer).not.toBe(buffer);
expect(result2.buffer).not.toBe(buffer);
expect(result1.buffer).not.toBe(result2.buffer);
});
});
it('should build interpolated strings', () => {
const t = template({ hello: '$Hello, ${name}!', not: 'Hello $name', array: ['$$name', '$not'] });
const result = t(
createVmContext({
name: 'world',
}),
);
expect(result).toEqual({
hello: 'Hello, world!',
not: 'Hello $name',
array: ['world', 'not'],
});
});
it('should ignore properties on the prototype chain', () => {
const obj = Object.create({ prototype: 'chain' }) as Record<string, unknown>;
obj['own'] = 'property';
const result = template(obj)();
expect(result).toEqual({ own: 'property' });
});
it('should ignore non-enumerable properties', () => {
const obj = Object.create(null, {
enumerable: {
value: 'property',
enumerable: false,
},
}) as Record<string, unknown>;
const result = template(obj)();
expect(result).toEqual({});
});
describe('should work while objectKeyMode is', () => {
it('template', () => {
const obj = { $: 1, _: 2, '=x': 3, $$y: 4 };
const result = template(obj, { objectKeyMode: 'template' })(createVmContext({}, { x: 11n, y: 12 }));
expect(result).toEqual({
$: 1,
_: 2,
11: 3,
12: 4,
});
});
it('ignore', () => {
const obj = { $: 1, _: 2, '=x': 3, $$y: 4 };
const result = template(obj, { objectKeyMode: 'ignore' })(createVmContext({}, { x: 11n, y: 12 }));
expect(result).toEqual({
$: 1,
_: 2,
'=x': 3,
$$y: 4,
});
});
});
describe('should work with evaluator', () => {
it('with formula', () => {
const obj = { a: '=a', b: '=b', complex: '=a + (b ?? 1)' };
const result = template(obj)(createVmContext({ a: 1, b: undefined }));
expect(result).toEqual({ a: 1, b: null, complex: 2 });
});
it('with interpolation', () => {
const obj = { a: '$$a', b: '$$b', complex: '$Value is ${a}-${b}' };
const result = template(obj)(createVmContext({ a: 1, b: undefined }));
expect(result).toEqual({ a: '1', b: '', complex: 'Value is 1-' });
});
it('has extern values', () => {
const obj = { a: '=ab', b: '$$(ta.length)', t: '=ta' };
const externValues = { ab: new ArrayBuffer(8), ta: new Uint8Array(4) };
const result = template(obj)(createVmContext({}, externValues));
expect(result).toEqual({ a: externValues.ab, b: '4', t: externValues.ta });
});
});
});