2
0
mirror of https://github.com/tenrok/BBob.git synced 2026-06-17 19:21:20 +03:00

feat: typescript support (#185)

* feat: initial typescript support

* feat: typescript support

* feat(plugin-helper): move files to typescript

* chore: update lock files

* feat: preset types

* fix: build

* fix: benchmark

* fix: remove pnpm cache

* fix: bench action

* fix: pnpm recursive install

* fix: nx cache

* fix: lock file

* fix: workflows

* fix: lerna support in pnpm

* fix: pnpm workspace

* fix: remove unused files

* fix: pnpm lock file

* fix: update lerna for support pnpm

* fix: lerna bootstrap

* fix: rollup build

* fix: update nx

* fix: build

* fix: add nx dep target

* fix: remove nx cache

* fix: workflow run on push only for master

* fix: test workflow run on push only for master

* fix: remove parallel for gen types

* fix: benchmark

* fix: benchmark imports

* fix: pnpm

* fix: types errors and pnpm

* fix: types

* fix: types

* refactor: parser

* fix(parser): tests

* fix: preset tests

* fix: react types

* fix: react type declarations

* fix: pnpm lock file

* fix: react preset types

* fix: lock file

* fix: vue2 types

* feat: dev container support

* fix: types

* fix: types

* refactor: rewrite pkg-task, add nx gen-types deps, fix react/render.ts

* refactor: types

* fix: types

* fix: rename gen-types to types

* fix: nx build order

* fix: nx reset

* fix: define nx deps explicit

* fix: build

* fix: nx

* fix: nx order build

* fix: nx deps

* fix: bbob cli tests

* fix: tests

* fix: cli tests and import

* fix: test cover

* fix: cli cover
This commit is contained in:
Nikolay Kost
2024-04-23 21:11:14 +02:00
committed by GitHub
parent 05246b2aea
commit 8797f7f363
149 changed files with 6102 additions and 3670 deletions
+4
View File
@@ -2,3 +2,7 @@ coverage
dist
lib
es
types
test/*.d.ts
test/*.map
tsconfig.tsbuildinfo
+1 -1
View File
@@ -1,4 +1,4 @@
package-lock.json
pnpm-lock.yaml
coverage
src
!dist
+52 -18
View File
@@ -7,11 +7,49 @@
"plugin",
"helper"
],
"files": [
"dist",
"lib",
"src",
"es",
"types"
],
"main": "lib/index.js",
"module": "es/index.js",
"jsnext:main": "es/index.js",
"browser": "dist/index.js",
"browserName": "BbobPluginHelper",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./es/index.js",
"require": "./lib/index.js",
"browser": "./dist/index.min.js",
"umd": "./dist/index.min.js"
},
"./char": {
"types": "./types/char.d.ts",
"import": "./es/char.js",
"require": "./lib/char.js",
"browser": "./dist/index.min.js",
"umd": "./dist/index.min.js"
},
"./helpers": {
"types": "./types/helpers.d.ts",
"import": "./es/helpers.js",
"require": "./lib/helpers.js",
"browser": "./dist/index.min.js",
"umd": "./dist/index.min.js"
},
"./TagNode": {
"types": "./types/TagNode.d.ts",
"import": "./es/TagNode.js",
"require": "./lib/TagNode.js",
"browser": "./dist/index.min.js",
"umd": "./dist/index.min.js"
}
},
"homepage": "https://github.com/JiLiZART/bbob",
"author": "Nikolay Kostyurin <jilizart@gmail.com>",
"license": "MIT",
@@ -23,35 +61,31 @@
"url": "git://github.com/JiLiZART/bbob.git"
},
"scripts": {
"build:commonjs": "../../scripts/pkg-task build-commonjs",
"build:es": "../../scripts/pkg-task build-es",
"build:umd": "../../scripts/pkg-task build-umd",
"build": "npm run build:commonjs && npm run build:es && npm run build:umd",
"test": "../../scripts/pkg-task test",
"cover": "../../scripts/pkg-task cover",
"lint": "../../scripts/pkg-task lint",
"size": "../../scripts/pkg-task size",
"bundlesize": "../../scripts/pkg-task bundlesize",
"build:commonjs": "pkg-task",
"build:es": "pkg-task",
"build:umd": "pkg-task",
"build": "pkg-task",
"test": "pkg-task",
"cover": "pkg-task",
"lint": "pkg-task",
"size": "pkg-task",
"bundlesize": "pkg-task",
"types": "pkg-task",
"prepublishOnly": "npm run build"
},
"size-limit": [
{
"path": "lib/index.js"
"path": "./dist/index.min.js",
"size": "1 KB"
}
],
"bundlesize": [
{
"path": "./dist/index.min.js",
"maxSize": "1024 B"
"maxSize": "1 KB"
}
],
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"files": [
"dist",
"lib",
"src",
"es"
]
}
}
@@ -1,77 +0,0 @@
import { OPEN_BRAKET, CLOSE_BRAKET, SLASH } from './char';
import {
getNodeLength, appendToNode, attrsToString, attrValue, getUniqAttr,
} from './helpers';
const getTagAttrs = (tag, params) => {
const uniqAattr = getUniqAttr(params);
if (uniqAattr) {
const tagAttr = attrValue(tag, uniqAattr);
const attrs = { ...params };
delete attrs[uniqAattr];
const attrsStr = attrsToString(attrs);
return `${tagAttr}${attrsStr}`;
}
return `${tag}${attrsToString(params)}`;
};
class TagNode {
constructor(tag, attrs, content) {
this.tag = tag;
this.attrs = attrs;
this.content = Array.isArray(content) ? content : [content];
}
attr(name, value) {
if (typeof value !== 'undefined') {
this.attrs[name] = value;
}
return this.attrs[name];
}
append(value) {
return appendToNode(this, value);
}
get length() {
return getNodeLength(this);
}
toTagStart({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) {
const tagAttrs = getTagAttrs(this.tag, this.attrs);
return `${openTag}${tagAttrs}${closeTag}`;
}
toTagEnd({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) {
return `${openTag}${SLASH}${this.tag}${closeTag}`;
}
toTagNode() {
return new TagNode(this.tag.toLowerCase(), this.attrs, this.content);
}
toString({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) {
const isEmpty = this.content.length === 0;
const content = this.content.reduce((r, node) => r + node.toString({ openTag, closeTag }), '');
const tagStart = this.toTagStart({ openTag, closeTag });
if (isEmpty) {
return tagStart;
}
return `${tagStart}${content}${this.toTagEnd({ openTag, closeTag })}`;
}
}
TagNode.create = (tag, attrs = {}, content = []) => new TagNode(tag, attrs, content);
TagNode.isOf = (node, type) => (node.tag === type);
export { TagNode };
export default TagNode;
+115
View File
@@ -0,0 +1,115 @@
import { OPEN_BRAKET, CLOSE_BRAKET, SLASH } from './char';
import {
getUniqAttr,
getNodeLength,
appendToNode,
attrsToString,
attrValue,
isTagNode,
} from './helpers';
import type { NodeContent, TagNodeObject, TagNodeTree } from "./types";
const getTagAttrs = <AttrValue>(tag: string, params: Record<string, AttrValue>) => {
const uniqAttr = getUniqAttr(params);
if (uniqAttr) {
const tagAttr = attrValue(tag, uniqAttr);
const attrs = { ...params };
delete attrs[String(uniqAttr)];
const attrsStr = attrsToString(attrs);
return `${tagAttr}${attrsStr}`;
}
return `${tag}${attrsToString(params)}`;
};
const renderContent = (content: TagNodeTree, openTag: string, closeTag: string) => {
const toString = (node: NodeContent) => {
if (isTagNode(node)) {
return node.toString({ openTag, closeTag })
}
return String(node)
}
if (Array.isArray(content)) {
return content.reduce<string>((r, node) => {
if (node !== null) {
return r + toString(node)
}
return r
}, '')
}
if (content) {
return toString(content)
}
return null
}
export class TagNode implements TagNodeObject {
public readonly tag: string
public attrs: Record<string, unknown>
public content: TagNodeTree
constructor(tag: string, attrs: Record<string, unknown>, content: TagNodeTree) {
this.tag = tag;
this.attrs = attrs;
this.content = content
}
attr(name: string, value?: unknown) {
if (typeof value !== 'undefined') {
this.attrs[name] = value;
}
return this.attrs[name];
}
append(value: string) {
return appendToNode(this, value);
}
get length(): number {
return getNodeLength(this);
}
toTagStart({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) {
const tagAttrs = getTagAttrs(this.tag, this.attrs);
return `${openTag}${tagAttrs}${closeTag}`;
}
toTagEnd({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) {
return `${openTag}${SLASH}${this.tag}${closeTag}`;
}
toTagNode() {
return new TagNode(this.tag.toLowerCase(), this.attrs, this.content);
}
toString({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}): string {
const content = this.content ? renderContent(this.content, openTag, closeTag) : ''
const tagStart = this.toTagStart({ openTag, closeTag });
if (this.content === null || Array.isArray(this.content) && this.content.length === 0) {
return tagStart;
}
return `${tagStart}${content}${this.toTagEnd({ openTag, closeTag })}`;
}
static create(tag: string, attrs: Record<string, unknown> = {}, content: TagNodeTree = []) {
return new TagNode(tag, attrs, content)
}
static isOf(node: TagNode, type: string) {
return (node.tag === type)
}
}
-100
View File
@@ -1,100 +0,0 @@
import { N } from './char';
const isTagNode = (el) => typeof el === 'object' && !!el.tag;
const isStringNode = (el) => typeof el === 'string';
const isEOL = (el) => el === N;
const keysReduce = (obj, reduce, def) => Object.keys(obj).reduce(reduce, def);
const getNodeLength = (node) => {
if (isTagNode(node)) {
return node.content.reduce((count, contentNode) => count + getNodeLength(contentNode), 0);
} if (isStringNode(node)) {
return node.length;
}
return 0;
};
/**
* Appends value to Tag Node
* @param {TagNode} node
* @param value
*/
const appendToNode = (node, value) => {
node.content.push(value);
};
/**
* Replaces " to &qquot;
* @param {String} value
*/
const escapeHTML = (value) => value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
// eslint-disable-next-line no-script-url
.replace(/(javascript|data|vbscript):/gi, '$1%3A');
/**
* Acept name and value and return valid html5 attribute string
* @param {String} name
* @param {String} value
* @return {string}
*/
const attrValue = (name, value) => {
const type = typeof value;
const types = {
boolean: () => (value ? `${name}` : ''),
number: () => `${name}="${value}"`,
string: () => `${name}="${escapeHTML(value)}"`,
object: () => `${name}="${escapeHTML(JSON.stringify(value))}"`,
};
return types[type] ? types[type]() : '';
};
/**
* Transforms attrs to html params string
* @param values
*/
const attrsToString = (values) => {
// To avoid some malformed attributes
if (values == null) {
return '';
}
return keysReduce(
values,
(arr, key) => [...arr, attrValue(key, values[key])],
[''],
).join(' ');
};
/**
* Gets value from
* @example
* getUniqAttr({ 'foo': true, 'bar': bar' }) => 'bar'
* @param attrs
* @returns {string}
*/
const getUniqAttr = (attrs) => keysReduce(
attrs,
(res, key) => (attrs[key] === key ? attrs[key] : null),
null,
);
export {
attrsToString,
attrValue,
appendToNode,
escapeHTML,
getNodeLength,
getUniqAttr,
isTagNode,
isStringNode,
isEOL,
};
+125
View File
@@ -0,0 +1,125 @@
import { N } from './char';
import type { TagNode } from "./TagNode";
import type { NodeContent, StringNode } from "./types";
function isTagNode(el: unknown): el is TagNode {
return typeof el === 'object' && el !== null && 'tag' in el;
}
function isStringNode(el: unknown): el is StringNode {
return typeof el === 'string';
}
// check string is end of line
function isEOL(el: string) {
return el === N
}
function keysReduce<Res, Def extends Res, T extends Record<string, unknown>>(obj: T, reduce: (acc: Def, key: keyof T) => Res, def: Def): Res {
const keys = Object.keys(obj)
return keys.reduce((acc, key) => reduce(acc, key), def)
}
function getNodeLength(node: NodeContent): number {
if (isTagNode(node) && Array.isArray(node.content)) {
return node.content.reduce<number>((count, contentNode) => {
return count + getNodeLength(contentNode)
}, 0);
}
if (isStringNode(node)) {
return String(node).length;
}
return 0;
}
function appendToNode(node: TagNode, value: NodeContent) {
if (Array.isArray(node.content)) {
node.content.push(value);
}
}
/**
* Replaces " to &qquot;
* @param {string} value
*/
function escapeAttrValue(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
// eslint-disable-next-line no-script-url
.replace(/(javascript|data|vbscript):/gi, '$1%3A');
}
/**
* @deprecated use escapeAttrValue
*/
const escapeHTML = escapeAttrValue
/**
* Accept name and value and return valid html5 attribute string
*/
function attrValue<AttrValue = unknown>(name: string, value: AttrValue) {
// in case of performance
switch (typeof value) {
case 'boolean':
return value ? `${name}` : ''
case 'number':
return `${name}="${value}"`
case 'string':
return `${name}="${escapeAttrValue(value as string)}"`
case 'object':
return `${name}="${escapeAttrValue(JSON.stringify(value))}"`
default:
return ''
}
}
/**
* Transforms attrs to html params string
* @example
* attrsToString({ 'foo': true, 'bar': bar' }) => 'foo="true" bar="bar"'
*/
function attrsToString<AttrValue = unknown>(values: Record<string, AttrValue> | null) {
// To avoid some malformed attributes
if (values == null) {
return '';
}
return keysReduce(
values,
(arr, key) => [...arr, attrValue(key, values[key])],
[''],
).join(' ');
}
/**
* Gets value from
* @example
* getUniqAttr({ 'foo': true, 'bar': bar' }) => 'bar'
*/
function getUniqAttr<Value>(attrs: Record<string, Value>) {
return keysReduce(
attrs,
(res, key) => (attrs[key] === key ? attrs[key] : null),
null,
)
}
export {
attrsToString,
attrValue,
appendToNode,
escapeHTML,
escapeAttrValue,
getNodeLength,
getUniqAttr,
isTagNode,
isStringNode,
isEOL,
};
-3
View File
@@ -1,3 +0,0 @@
export * from './helpers';
export * from './char';
export { TagNode } from './TagNode';
+4
View File
@@ -0,0 +1,4 @@
export * from "./helpers";
export * from "./char";
export * from "./TagNode";
export * from "./types";
+13
View File
@@ -0,0 +1,13 @@
export type StringNode = string | number
export interface TagNodeObject {
readonly tag: string
attrs: Record<string, unknown>
content: TagNodeTree
}
export type NodeContent = TagNodeObject | StringNode | null
export type PartialNodeContent = Partial<TagNodeObject> | StringNode | null
export type TagNodeTree = NodeContent | NodeContent[] | null
@@ -13,6 +13,37 @@ describe('@bbob/plugin-helper/TagNode', () => {
expect(TagNode.isOf(tagNode, 'test')).toBe(true);
});
test('attr', () => {
const tagNode = TagNode.create('test', {test: 1}, ['Hello']);
tagNode.attr('foo', 'bar')
expect(tagNode.attrs.foo).toBe('bar');
});
test('append', () => {
const tagNode = TagNode.create('test', {test: 1}, ['Hello']);
tagNode.append('World')
expect(tagNode.content).toEqual(['Hello', 'World']);
});
test('length', () => {
const tagNode = TagNode.create('test', {test: 1}, ['Hello', 'World']);
expect(tagNode.length).toEqual('HelloWorld'.length);
});
test('toTagNode', () => {
const tagNode = TagNode.create('test', {test: 1}, ['Hello']);
const newTagNode = tagNode.toTagNode()
expect(newTagNode !== tagNode).toBe(true);
expect(newTagNode.tag).toEqual(tagNode.tag);
expect(newTagNode.content).toEqual(tagNode.content);
});
describe('toString', () => {
test('tag with content and params', () => {
const tagNode = TagNode.create('test', {test: 1}, ['Hello']);
@@ -1,116 +0,0 @@
import {
attrsToString,
attrValue,
appendToNode,
getNodeLength,
getUniqAttr,
isTagNode,
isStringNode,
isEOL,
} from '../src';
describe('@bbob/plugin-helper/helpers', () => {
test('appendToNode', () => {
const value = 'test';
const node = { content: [] };
appendToNode(node, value);
expect(node.content.pop()).toBe(value);
});
test('getNodeLength', () => {
const node = {
tag: 'test',
content: [
'123',
{
tag: 'test2',
content: ['123']
}
]
};
expect(getNodeLength(node)).toBe(6)
});
test('isTagNode', () => {
const node = {
tag: 'test',
content: []
};
expect(isTagNode(node)).toBe(true)
});
test('isStringNode', () => {
const node = {
tag: 'test',
content: ['123']
};
expect(isStringNode(node.content[0])).toBe(true);
});
test('attrValue boolean', () => {
expect(attrValue('test', true)).toBe('test');
});
test('attrValue number', () => {
expect(attrValue('test', 123)).toBe('test="123"');
});
test('attrValue string', () => {
expect(attrValue('test', 'hello')).toBe('test="hello"');
});
test('attrValue object', () => {
const attrs = { tag: 'test' };
expect(attrValue('test', attrs)).toBe('test="{&quot;tag&quot;:&quot;test&quot;}"');
});
test('isEOL', () => {
expect(isEOL('\n')).toBe(true)
});
test('attrsToString', () => {
expect(attrsToString({
tag: 'test',
foo: 'bar',
disabled: true
})).toBe(` tag="test" foo="bar" disabled`)
});
test('attrsToString undefined', () => {
expect(attrsToString(undefined)).toBe('')
});
describe('attrsToString escape', () => {
test(`javascript:alert("hello")`, () => {
expect(attrsToString({
onclick: `javascript:alert('hello')`,
href: `javascript:alert('hello')`,
})).toBe(` onclick="javascript%3Aalert(&#039;hello&#039;)" href="javascript%3Aalert(&#039;hello&#039;)"`)
});
test(`JAVASCRIPT:alert("hello")`, () => {
expect(attrsToString({
onclick: `JAVASCRIPT:alert('hello')`,
href: `JAVASCRIPT:alert('hello')`,
})).toBe(` onclick="JAVASCRIPT%3Aalert(&#039;hello&#039;)" href="JAVASCRIPT%3Aalert(&#039;hello&#039;)"`)
});
test(`<tag>`, () => {
expect(attrsToString({
onclick: `<tag>`,
href: `<tag>`,
})).toBe(` onclick="&lt;tag&gt;" href="&lt;tag&gt;"`)
});
});
test('getUniqAttr with unq attr', () => {
expect(getUniqAttr({foo: true, 'http://bar.com': 'http://bar.com'})).toBe('http://bar.com')
});
test('getUniqAttr without unq attr', () => {
expect(getUniqAttr({foo: true})).toBe(null)
})
});
@@ -0,0 +1,117 @@
import {
attrsToString,
attrValue,
appendToNode,
getNodeLength,
getUniqAttr,
isTagNode,
isStringNode,
isEOL,
TagNode,
} from '../src';
describe('@bbob/plugin-helper/helpers', () => {
test('appendToNode', () => {
const value = 'test';
const node = {content: []} as TagNode;
appendToNode(node, value);
expect(node.content.pop()).toBe(value);
});
test('getNodeLength', () => {
const node = {
tag: 'test',
content: [
'123',
{
tag: 'test2',
content: ['123']
}
]
} as TagNode;
expect(getNodeLength(node)).toBe(6)
});
test('isTagNode', () => {
const node = {
tag: 'test',
content: []
} as TagNode;
expect(isTagNode(node)).toBe(true)
});
test('isStringNode', () => {
const node = {
tag: 'test',
content: ['123']
};
expect(isStringNode(node.content[0])).toBe(true);
});
test('attrValue boolean', () => {
expect(attrValue('test', true)).toBe('test');
});
test('attrValue number', () => {
expect(attrValue('test', 123)).toBe('test="123"');
});
test('attrValue string', () => {
expect(attrValue('test', 'hello')).toBe('test="hello"');
});
test('attrValue object', () => {
const attrs = {tag: 'test'};
expect(attrValue('test', attrs)).toBe('test="{&quot;tag&quot;:&quot;test&quot;}"');
});
test('isEOL', () => {
expect(isEOL('\n')).toBe(true)
});
test('attrsToString', () => {
expect(attrsToString({
tag: 'test',
foo: 'bar',
disabled: true
})).toBe(` tag="test" foo="bar" disabled`)
});
test('attrsToString undefined', () => {
expect(attrsToString(undefined)).toBe('')
});
describe('attrsToString escape', () => {
test(`javascript:alert("hello")`, () => {
expect(attrsToString({
onclick: `javascript:alert('hello')`,
href: `javascript:alert('hello')`,
})).toBe(` onclick="javascript%3Aalert(&#039;hello&#039;)" href="javascript%3Aalert(&#039;hello&#039;)"`)
});
test(`JAVASCRIPT:alert("hello")`, () => {
expect(attrsToString({
onclick: `JAVASCRIPT:alert('hello')`,
href: `JAVASCRIPT:alert('hello')`,
})).toBe(` onclick="JAVASCRIPT%3Aalert(&#039;hello&#039;)" href="JAVASCRIPT%3Aalert(&#039;hello&#039;)"`)
});
test(`<tag>`, () => {
expect(attrsToString({
onclick: `<tag>`,
href: `<tag>`,
})).toBe(` onclick="&lt;tag&gt;" href="&lt;tag&gt;"`)
});
});
test('getUniqAttr with unq attr', () => {
expect(getUniqAttr({foo: true, 'http://bar.com': 'http://bar.com'})).toBe('http://bar.com')
});
test('getUniqAttr without unq attr', () => {
expect(getUniqAttr({foo: true})).toBe(null)
})
});
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "./types"
},
"include": [
"./src/**/*"
]
}