chore: resolve conflict

This commit is contained in:
haiqin 2024-01-27 09:49:41 +08:00
commit 7b6713bc6c
25 changed files with 1757 additions and 5 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ package-lock.json
pnpm-lock.yaml
/packages/**/node_modules
dist
.history

View File

@ -41,16 +41,30 @@ describe('test no-vdom', () => {
const CountingComponent = () => {
const count = reactive(0);
g_count = count;
let View
return (() => {
const _el$ = _tmpl$(),
watch: if (count > 0) {
View.$viewValue = createView(() => {})
}
View = createView((() => {
const _el$ = tmp.cloneNode(true),
_el$2 = _el$.firstChild,
_el$4 = _el$2.nextSibling,
_el$3 = _el$4.nextSibling;
_$insert(_el$, count, _el$4);
return _el$;
})();
_$insert(_el$, count, _el$4);
return _el$;
})());
return View
};
function createView(el) {
Object.defineProperty(el, '$viewValue', {
set: value => {
el.replaceWith(value);
}
})
}
render(() => _$createComponent(CountingComponent, {}), container);

View File

@ -0,0 +1,49 @@
{
"name": "babel-preset-inula-jsx",
"version": "0.0.0",
"description": "Inula jsx transpiler babel preset",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"inula",
"babel-preset"
],
"files": [
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap",
"test": "vitest --ui"
},
"devDependencies": {
"@types/babel__core": "^7.20.5",
"@types/node": "^20.10.5",
"@vitest/ui": "^1.2.1",
"tsup": "^6.7.0",
"typescript": "^5.3.2",
"vitest": "^1.2.1"
},
"dependencies": {
"@babel/plugin-syntax-jsx": "7.16.7",
"babel-plugin-syntax-typescript-new": "^1.0.0",
"minimatch": "^9.0.3"
},
"tsup": {
"entry": [
"src/index.ts"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"dts": true,
"minify": true
}
}

View File

@ -0,0 +1 @@
declare module "@babel/plugin-syntax-jsx"

View File

@ -0,0 +1,21 @@
import syntaxTypescript from 'babel-plugin-syntax-typescript-new';
import syntaxJSX from '@babel/plugin-syntax-jsx';
import { InulaOption } from './types';
import type { ConfigAPI, TransformOptions } from '@babel/core';
import inula from './plugin';
export default function (
_: ConfigAPI,
options: InulaOption
): TransformOptions {
return {
plugins: [
[syntaxJSX.default ?? syntaxJSX],
[syntaxTypescript, {isJsx: true}],
[inula, options],
],
};
}
export type { InulaOption };

View File

@ -0,0 +1,22 @@
import { PluginObj } from '@babel/core';
import { InulaOption } from './types';
import * as babel from '@babel/core';
import { PluginProvider } from './pluginProvider';
export default function (api: typeof babel, options: InulaOption): PluginObj {
const pluginProvider = new PluginProvider(api, options);
return {
name: 'babel-plugin-inula-jsx',
visitor: {
Program: {
enter(path) {
console.log('babel-plugin-inula-jsx: Program enter');
},
exit(path) {
console.log('babel-plugin-inula-jsx: Program exit');
},
},
},
};
}

View File

@ -0,0 +1,106 @@
import type { types as t, NodePath } from '@babel/core';
import type babel from '@babel/core';
import { minimatch } from 'minimatch';
import { InulaOption } from './types';
export class PluginProvider {
private static readonly inulaPackageName = 'inula';
// ---- Plugin Level ----
private readonly babelApi: typeof babel
private readonly t: typeof t
private readonly traverse: typeof babel.traverse
private readonly includes: string[]
private readonly excludes: string[]
constructor(babelApi: typeof babel, options: InulaOption) {
this.babelApi = babelApi;
this.t = babelApi.types;
this.traverse = babelApi.traverse;
const includes = options.files ?? ['**/*.{jsx,tsx}'];
const excludes = options.excludeFiles ?? ['**/{dist,node_modules,lib}'];
this.includes = Array.isArray(includes) ? includes : [includes];
this.excludes = Array.isArray(excludes) ? excludes : [excludes];
}
// ---- Two levels of enter:
// 1. File Level: controlled by includes/excludes
// 2. File Level: controlled by imports(must import inula)
private fileEnter = true
// ---- File Level
private programNode?: t.Program
private allImports: t.ImportDeclaration[] = []
private didAlterImports = false
private transformedCount = 0
// ---- Component Level ----
programEnterVisitor(
path: NodePath<t.Program>,
filename: string | undefined
): void {
this.fileEnter = this.fileAllowed(filename);
if (!this.fileEnter) return;
this.allImports = path.node.body.filter(n =>
this.t.isImportDeclaration(n)
) as t.ImportDeclaration[];
const inulaImports = this.allImports.filter(
n => n.source.value === PluginProvider.inulaPackageName
);
// ---- @Enter2
if (inulaImports.length === 0) {
this.fileEnter = false;
return;
}
this.programNode = path.node;
this.transformedCount = 0;
}
programExitVisitor(path: NodePath<t.Program>): void {
if (!this.fileEnter) return;
}
jsxElementVisitor(path: NodePath<t.JSXElement>): void {
}
/**
* @brief Test if the file is allowed to be transformed
* @enter1
* @param path
* @param fileName
* @returns is file allowed
*/
private fileAllowed(fileName: string | undefined): boolean {
if (this.includes.includes('*')) return true;
// ---- Filename is not provided for web-based standalone transform
if (!fileName) return true;
if (this.excludes.some(pattern => minimatch(fileName, pattern)))
return false;
if (!this.includes.some(pattern => minimatch(fileName, pattern)))
return false;
return true;
}
/**
* @brief Wrap the value in a file
* @param node
* @returns wrapped value
*/
private wrapWithFile(node: t.Expression | t.Statement): t.File {
return this.t.file(
this.t.program([
this.t.isStatement(node) ? node : this.t.expressionStatement(node),
])
);
}
}

View File

@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest';
import { transformInula } from './mock';
describe('Entering', () => {
it('should use inula jsx preset in babel', () => {
const code = 'console.log(\'hello world\');';
expect(transformInula(code)).toBe(code);
});
});

View File

@ -0,0 +1,8 @@
import babel, { transform, types as t } from '@babel/core';
import inula from '../';
export function transformInula(code: string) {
return transform(code, {
presets: [inula]
})?.code;
}

View File

@ -0,0 +1,13 @@
export interface InulaOption {
/**
* Files that will be included
* @default ** /*.{jsx,tsx}
*/
files?: string | string[]
/**
* Files that will be excludes
* @default ** /{dist,node_modules,lib}/*.{js,ts}
*/
excludeFiles?: string | string[]
}

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -0,0 +1,44 @@
{
"name": "@inula/view-parser",
"version": "0.0.0",
"description": "Inula jsx parser",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"inula"
],
"files": [
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap",
"test": "vitest --ui"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@types/babel__core": "^7.20.5",
"@vitest/ui": "^0.34.5",
"tsup": "^6.7.0",
"typescript": "^5.3.2",
"@babel/plugin-syntax-jsx": "7.16.7",
"vitest": "^0.34.5"
},
"tsup": {
"entry": [
"src/index.ts"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"dts": true,
"minify": true
}
}

View File

@ -0,0 +1,17 @@
import { ViewParser } from './parser';
import type { ViewUnit, ViewParserConfig, AllowedJSXNode } from './types';
/**
* @brief Generate view units from a babel ast
* @param statement
* @param config
* @returns ViewUnit[]
*/
export function parseView(
node: AllowedJSXNode,
config: ViewParserConfig
): ViewUnit[] {
return new ViewParser(config).parse(node);
}
export type * from './types';

View File

@ -0,0 +1,512 @@
import type { NodePath, types as t, traverse as tr } from '@babel/core';
import type {
UnitProp,
ViewUnit,
ViewParserConfig,
AllowedJSXNode,
HTMLUnit,
TextUnit,
MutableUnit,
TemplateProp,
} from './types';
export class ViewParser {
// ---- Namespace and tag name
private readonly htmlNamespace: string = 'html'
private readonly htmlTagNamespace: string = 'tag'
private readonly compTagNamespace: string = 'comp'
private readonly envTagName: string = 'env'
private readonly ifTagName: string = 'if'
private readonly elseIfTagName: string = 'else-if'
private readonly elseTagName: string = 'else'
private readonly customHTMLProps: string[] = ['ref']
private readonly config: ViewParserConfig
private readonly htmlTags: string[]
private readonly willParseTemplate: boolean
private readonly t: typeof t
private readonly traverse: typeof tr
private readonly viewUnits: ViewUnit[] = []
/**
* @brief Constructor
* @param config
*/
constructor(config: ViewParserConfig) {
this.config = config;
this.t = config.babelApi.types;
this.traverse = config.babelApi.traverse;
this.htmlTags = config.htmlTags;
this.willParseTemplate = config.parseTemplate ?? true;
}
/**
* @brief Parse the node into view units
* @param node
* @returns ViewUnit[]
*/
parse(node: AllowedJSXNode): ViewUnit[] {
if (this.t.isJSXText(node)) this.parseText(node);
else if (this.t.isJSXExpressionContainer(node)) this.parseExpression(node.expression);
else if (this.t.isJSXElement(node)) this.parseElement(node);
else if (this.t.isJSXFragment(node)) {
node.children.forEach(child => {
this.parse(child);
});
}
return this.viewUnits;
}
/**
* @brief Parse JSXText
* @param node
*/
private parseText(node: t.JSXText): void {
const text = node.value.trim();
if (!text) return;
this.viewUnits.push({
type: 'text',
content: this.t.stringLiteral(text),
});
}
/**
* @brief Parse JSXExpressionContainer
* @param node
*/
private parseExpression(node: t.Expression | t.JSXEmptyExpression): void {
if (this.t.isJSXEmptyExpression(node)) return;
if (this.t.isLiteral(node) && !this.t.isTemplateLiteral(node)) {
// ---- Treat literal as text except template literal
// Cuz template literal may have viewProp inside like:
// <>{i18n`hello ${<MyView/>}`}</>
this.viewUnits.push({
type: 'text',
content: node
});
return;
}
this.viewUnits.push({
type: 'exp',
content: this.parseProp(node),
});
}
/**
* @brief Parse JSXElement
* @param node
*/
private parseElement(node: t.JSXElement): void {
let type: 'html' | 'comp';
let tag: t.Expression;
// ---- Parse tag and type
const openingName = node.openingElement.name;
if (this.t.isJSXIdentifier(openingName)) {
// ---- Opening name is a JSXIdentifier, e.g., <div>
const name = openingName.name;
// ---- Specially parse if and env
if ([this.ifTagName, this.elseIfTagName, this.elseTagName].includes(name))
return this.parseIf(node);
if (name === this.envTagName) return this.parseEnv(node);
else if (this.htmlTags.includes(name)) {
type = 'html';
tag = this.t.stringLiteral(name);
} else {
// ---- If the name is not in htmlTags, treat it as a comp
type = 'comp';
tag = this.t.identifier(name);
}
} else if (this.t.isJSXMemberExpression(openingName)) {
// ---- Opening name is a JSXMemberExpression, e.g., <Comp.Div>
// Treat it as a comp and set the tag as the opening name
type = 'comp';
// ---- Turn JSXMemberExpression into MemberExpression recursively
const toMemberExpression = (node: t.JSXMemberExpression): t.MemberExpression => {
if (this.t.isJSXMemberExpression(node.object)) {
return this.t.memberExpression(
toMemberExpression(node.object),
this.t.identifier(node.property.name)
);
}
return this.t.memberExpression(
this.t.identifier(node.object.name),
this.t.identifier(node.property.name)
);
};
tag = toMemberExpression(openingName);
} else {
// ---- isJSXNamespacedName
const namespace = openingName.namespace.name;
switch (namespace) {
case this.compTagNamespace:
// ---- If the namespace is the same as the compTagNamespace, treat it as a comp
// and set the tag as an identifier
// e.g., <comp:div/> => ["comp", div]
// this means you've declared a component named "div" and force it to be a comp instead an html
type = 'comp';
tag = this.t.identifier(openingName.name.name);
break;
case this.htmlNamespace:
// ---- If the namespace is the same as the htmlTagNamespace, treat it as an html
// and set the tag as a string literal
// e.g., <html:MyWebComponent/> => ["html", "MyWebComponent"]
// the tag will be treated as a string, i.e., <MyWebComponent/>
type = 'html';
tag = this.t.stringLiteral(openingName.name.name);
break;
case this.htmlTagNamespace:
// ---- If the namespace is the same as the htmlTagNamespace, treat it as an html
// and set the tag as an identifier
// e.g., <tag:variable/> => ["html", variable]
// this unit will be htmlUnit and the html string tag is stored in "variable"
type = 'html';
tag = this.t.identifier(openingName.name.name);
break;
default:
// ---- Otherwise, treat it as an html tag and make the tag as the namespace:name
type = 'html';
tag = this.t.stringLiteral(`${namespace}:${openingName.name.name}`);
break;
}
}
// ---- Parse the props
const props = node.openingElement.attributes;
const propMap: Record<string, UnitProp> = Object.fromEntries(
props.map(prop => this.parseJSXProp(prop))
);
// ---- Parse the children
const childUnits = node.children.map(child => this.parseView(child)).flat();
let unit: ViewUnit = { type, tag, props: propMap, children: childUnits };
if (unit.type === 'html' && this.willParseTemplate)
unit = this.transformTemplate(unit);
this.viewUnits.push(unit);
}
/**
* @brief Parse EnvUnit
* @param node
*/
private parseEnv(node: t.JSXElement): void {
const props = node.openingElement.attributes;
const propMap: Record<string, UnitProp> = Object.fromEntries(
props.map(prop => this.parseJSXProp(prop))
);
const children = node.children.map(child => this.parseView(child)).flat();
this.viewUnits.push({
type: 'env',
props: propMap,
children,
});
}
private parseIf(node: t.JSXElement): void {
const name = (node.openingElement.name as t.JSXIdentifier).name;
// ---- else
if (name === this.elseTagName) {
const lastUnit = this.viewUnits[this.viewUnits.length - 1];
if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`);
lastUnit.branches.push({
condition: this.t.booleanLiteral(true),
children: node.children.map(child => this.parseView(child)).flat(),
});
return;
}
const condition = node.openingElement.attributes.filter(attr =>
this.t.isJSXAttribute(attr) && attr.name.name === 'cond'
)[0];
if (!condition) throw new Error(`Missing condition for ${name}`);
if (!this.t.isJSXAttribute(condition)) throw new Error(`JSXSpreadAttribute is not supported for ${name} condition`);
if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression))
throw new Error(`Invalid condition for ${name}`);
// ---- if
if (name === this.ifTagName) {
this.viewUnits.push({
type: 'if',
branches: [{
condition: condition.value.expression,
children: node.children.map(child => this.parseView(child)).flat(),
}],
});
return;
}
// ---- else-if
const lastUnit = this.viewUnits[this.viewUnits.length - 1];
if (!lastUnit || lastUnit.type !== 'if')
throw new Error(`Missing if for ${name}`);
lastUnit.branches.push({
condition: condition.value.expression,
children: node.children.map(child => this.parseView(child)).flat(),
});
}
/**
* @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp,
* considering both namespace and expression
* @param prop
* @returns [propName, propValue]
*/
private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] {
if (this.t.isJSXAttribute(prop)) {
let propName: string, specifier: string | undefined;
if (this.t.isJSXNamespacedName(prop.name)) {
// ---- If the prop name is a JSXNamespacedName, e.g., bind:value
// give it a special tag
propName = prop.name.name.name;
specifier = prop.name.namespace.name;
} else {
propName = prop.name.name;
}
let value = this.t.isJSXExpressionContainer(prop.value) ? prop.value.expression : prop.value;
if (this.t.isJSXEmptyExpression(value)) value = undefined;
return [propName, this.parseProp(value, specifier)];
}
// ---- Use *spread* as the propName to avoid conflict with other props
return ['*spread*', this.parseProp(prop.argument)];
}
/**
* @brief Parse the prop node into UnitProp
* @param propNode
* @param specifier
* @returns UnitProp
*/
private parseProp(propNode: t.Expression | undefined | null, specifier?: string): UnitProp {
// ---- If there is no propNode, set the default prop as true
if (!propNode) {
return {
value: this.t.booleanLiteral(true),
viewPropMap: {},
};
}
// ---- Collect sub jsx nodes as Prop
const viewPropMap: Record<string, ViewUnit[]> = {};
const parseViewProp = (innerPath: NodePath<t.JSXElement|t.JSXFragment>): void => {
const id = this.uid();
const node = innerPath.node;
viewPropMap[id] = this.parseView(node);
const newNode = this.t.stringLiteral(id);
if (node === propNode) {
// ---- If the node is the propNode, replace it with the new node
propNode = newNode;
}
// ---- Replace the node and skip the inner path
innerPath.replaceWith(newNode);
innerPath.skip();
};
// ---- Apply the parseViewProp to JSXElement and JSXFragment
this.traverse(this.wrapWithFile(propNode), {
JSXElement: parseViewProp,
JSXFragment: parseViewProp,
});
return {
value: propNode,
viewPropMap,
specifier,
};
}
private transformTemplate(unit: ViewUnit): ViewUnit {
if (!this.isHTMLTemplate(unit)) return unit;
unit = unit as HTMLUnit;
return {
type: 'template',
template: this.generateTemplate(unit),
mutableUnits: this.generateMutableUnits(unit),
props: this.parseTemplateProps(unit),
};
}
/**
* @brief Generate the entire HTMLUnit
* @param unit
* @returns HTMLUnit
*/
private generateTemplate(unit: HTMLUnit): HTMLUnit {
const staticProps = Object.fromEntries(this.filterTemplateProps(
// ---- Get all the static props
Object.entries(unit.props ?? []).filter(
([, prop]) =>
this.isStaticProp(prop) &&
// ---- Filter out props with false values
!(this.t.isBooleanLiteral(prop.value) && !prop.value.value)
)
));
let children: (HTMLUnit | TextUnit)[] | undefined;
if (unit.children) {
children = unit.children
.map(unit => {
if (unit.type === 'text') return unit;
if (unit.type === 'html' && this.t.isStringLiteral(unit.tag)) {
return this.generateTemplate(unit);
}
})
.filter(Boolean) as (HTMLUnit | TextUnit)[];
}
return {
type: 'html',
tag: unit.tag,
props: staticProps,
children,
};
}
/**
* @brief Collect all the mutable nodes in a static HTMLUnit
* We use this function to collect mutable nodes' path and props,
* so that in the generator, we know which position to insert the mutable nodes
* @param htmlUnit
* @returns mutable particles
*/
private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] {
const mutableUnits: MutableUnit[] = [];
const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => {
// ---- Generate mutable unit for current HTMLUnit
unit.children?.forEach((child, idx) => {
if (
!(child.type === 'html' && this.t.isStringLiteral(child.tag)) &&
!(child.type === 'text')
) {
mutableUnits.push({
path: [...path, idx],
...this.transformTemplate(child),
});
}
});
// ---- Recursively generate mutable units for static HTMLUnit children
unit.children
?.filter(
child => child.type === 'html' && this.t.isStringLiteral(child.tag)
)
.forEach((child, idx) => {
generateMutableUnit(child as HTMLUnit, [...path, idx]);
});
};
generateMutableUnit(htmlUnit);
return mutableUnits;
}
/**
* @brief Collect all the props in a static HTMLUnit or its nested HTMLUnit children
* Just like the mutable nodes, props are also equipped with path,
* so that we know which HTML ChildNode to insert the props
* @param htmlUnit
* @returns props
*/
private parseTemplateProps(htmlUnit: HTMLUnit): TemplateProp[] {
const templateProps: TemplateProp[] = [];
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
// ---- Generate all non-static(string/number/boolean) props for current HTMLUnit
// to be inserted further in the generator
unit.props && Object.entries(unit.props)
.filter(([, prop]) => !this.isStaticProp(prop))
.forEach(([key, prop]) => {
templateProps.push({
name: (unit.tag as t.StringLiteral).value,
key,
path,
value: prop.value,
});
});
// ---- Recursively generate props for static HTMLUnit children
unit.children
?.forEach((child, idx) => {
if (child.type !== 'html') return;
generateVariableProp(child, [...path, idx]);
});
};
generateVariableProp(htmlUnit, []);
return templateProps;
}
/**
* @brief Check if a ViewUnit is a static HTMLUnit that can be parsed into a template
* Must satisfy:
* 1. type is html
* 2. tag is a string literal, i.e., non-dynamic tag
* 3. has at least one child that is a static HTMLUnit,
* or else just call a createElement function, no need for template clone
* @param viewUnit
* @returns is a static HTMLUnit
*/
private isHTMLTemplate(viewUnit: ViewUnit): boolean {
return (
viewUnit.type === 'html' &&
this.t.isStringLiteral(viewUnit.tag) &&
!!viewUnit.children?.some(
child => child.type === 'html' && this.t.isStringLiteral(child.tag)
)
);
}
private isStaticProp(prop: UnitProp): boolean {
return (
this.t.isStringLiteral(prop.value) ||
this.t.isNumericLiteral(prop.value) ||
this.t.isBooleanLiteral(prop.value) ||
this.t.isNullLiteral(prop.value)
);
}
/**
* @brief Filter out some props that are not needed in the template,
* these are all special props to be parsed differently in the generator
* @param props
* @returns filtered props
*/
private filterTemplateProps<T>(
props: Array<[string, T]>
): Array<[string, T]> {
return (
props
// ---- Filter out event listeners
.filter(([key]) => !key.startsWith('on'))
// ---- Filter out specific props
.filter(([key]) => !this.customHTMLProps.includes(key))
);
}
/**
* @brief Parse the view by duplicating current parser's classRootPath, statements and htmlTags
* @param statements
* @returns ViewUnit[]
*/
private parseView(node: AllowedJSXNode): ViewUnit[] {
return new ViewParser(this.config).parse(node);
}
/**
* @brief Wrap the value in a file
* @param node
* @returns wrapped value
*/
private wrapWithFile(node: t.Expression): t.File {
return this.t.file(this.t.program([this.t.expressionStatement(node)]));
}
/**
* @brief Generate a unique id
* @returns a unique id
*/
private uid(): string {
return Math.random().toString(36).slice(2);
}
}

View File

@ -0,0 +1,205 @@
import { describe, expect, it, afterAll, beforeAll } from 'vitest';
import { config, parse, parseCode, parseView } from './mock';
import { types as t } from '@babel/core';
import type { CompUnit, HTMLUnit } from '../index';
describe('ElementUnit', () => {
beforeAll(() => {
// ---- Ignore template for this test
config.parseTemplate = false;
});
afterAll(() => {
config.parseTemplate = true;
});
// ---- Type
it('should identify a JSX element with tag in htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<div></div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should identify a JSX element with tag not in htmlTags as an CompUnit', () => {
const viewUnits = parse('<Comp></Comp>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('comp');
});
it('should identify a JSX element with namespaced "html" outside htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<html:MyWebComponent></html:MyWebComponent>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should identify a JSX element with namespaced "tag" outside htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<tag:variable></tag:variable>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should identify a JSX element with namespaced "comp" inside htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<comp:div></comp:div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('comp');
});
it('should identify a JSX element with name equal to "env" as an EnvUnit', () => {
const viewUnits = parse('<env></env>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('env');
});
// ---- Tag
it('should correctly parse the tag of an HTMLUnit', () => {
const viewUnits = parse('<div></div>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isStringLiteral(tag, { value: 'div' })).toBeTruthy();
});
it('should correctly parse the tag of an HTMLUnit with namespaced "html"', () => {
const viewUnits = parse('<html:MyWebComponent></html:MyWebComponent>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isStringLiteral(tag, { value: 'MyWebComponent' })).toBeTruthy();
});
it('should correctly parse the tag of an HTMLUnit with namespaced "tag"', () => {
const viewUnits = parse('<tag:variable></tag:variable>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isIdentifier(tag, { name: 'variable' })).toBeTruthy();
});
it('should correctly parse the tag of an CompUnit', () => {
const viewUnits = parse('<Comp></Comp>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isIdentifier(tag, { name: 'Comp' })).toBeTruthy();
});
it('should correctly parse the tag of an CompUnit with namespaced "comp"', () => {
const viewUnits = parse('<comp:div></comp:div>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isIdentifier(tag, { name: 'div' })).toBeTruthy();
});
// ---- Props(for both HTMLUnit and CompUnit)
it('should correctly parse the props', () => {
const viewUnits = parse('<div id="myId"></div>');
const htmlUnit = viewUnits[0] as HTMLUnit;
const props = htmlUnit.props!;
expect(
t.isStringLiteral(props.id.value, { value: 'myId' })
).toBeTruthy();
});
it('should correctly parse the props with a complex expression', () => {
const ast = parseCode('<div onClick={() => {console.log("ok")}}></div>');
const viewUnits = parseView(ast);
const originalExpression = ((
(ast as t.JSXElement).openingElement.attributes[0] as t.JSXAttribute
).value as t.JSXExpressionContainer).expression;
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.props!.onClick.value).toBe(originalExpression);
});
it('should correctly parse multiple props', () => {
const viewUnits = parse('<div id="myId" class="myClass"></div>');
const htmlUnit = viewUnits[0] as HTMLUnit;
const props = htmlUnit.props!;
expect(Object.keys(props).length).toBe(2);
expect(
t.isStringLiteral(props.id.value, { value: 'myId' })
).toBeTruthy();
expect(
t.isStringLiteral(props.class.value, { value: 'myClass' })
).toBeTruthy();
});
it('should correctly parse props with namespace as its specifier', () => {
const viewUnits = parse('<div bind:id="myId"></div>');
const htmlUnit = viewUnits[0] as HTMLUnit;
const props = htmlUnit.props!;
expect(props.id.specifier).toBe('bind');
expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy();
});
it('should correctly parse spread props', () => {
const viewUnits = parse('<Comp {...props}></Comp>');
const htmlUnit = viewUnits[0] as CompUnit;
const props = htmlUnit.props!;
expect(t.isIdentifier(props['*spread*'].value, { name: 'props' })).toBeTruthy();
});
// ---- View prop (other test cases can be found in ExpUnit.test.ts)
it('should correctly parse sub jsx attribute as view prop', () => {
const ast = parseCode('<Comp sub=<div>Ok</div>></Comp>');
const viewUnits = parseView(ast);
const props = (viewUnits[0] as CompUnit).props!;
const viewPropMap = props.sub.viewPropMap!;
expect(Object.keys(viewPropMap).length).toBe(1);
const key = Object.keys(viewPropMap)[0];
const viewProp = viewPropMap[key];
expect(viewProp.length).toBe(1);
expect(viewProp[0].type).toBe('html');
// ---- Prop View will be replaced with a random string and stored in props.viewPropMap
const value = props.sub.value;
expect(t.isStringLiteral(value, { value: key })).toBeTruthy();
});
// ---- Children(for both HTMLUnit and CompUnit)
it('should correctly parse the count of children', () => {
const viewUnits = parse(`<div>
<div>ok</div>
<div>ok</div>
<Comp></Comp>
<Comp></Comp>
</div>`);
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.children!.length).toBe(4);
});
it('should correctly parse the count of children with JSXExpressionContainer', () => {
const viewUnits = parse(`<div>
<div>ok</div>
<div>ok</div>
{count}
{count}
</div>`);
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.children!.length).toBe(4);
});
it('should correctly parse the count of children with JSXFragment', () => {
const viewUnits = parse(`<div>
<div>ok</div>
<div>ok</div>
<>
<Comp></Comp>
<Comp></Comp>
</>
</div>`);
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.children!.length).toBe(4);
});
it('should correctly parse the children', () => {
const viewUnits = parse('<div><div>ok</div></div>');
const htmlUnit = viewUnits[0] as HTMLUnit;
const firstChild = htmlUnit.children![0];
expect(firstChild.type).toBe('html');
expect(t.isStringLiteral((firstChild as HTMLUnit).tag, { value: 'div' })).toBeTruthy();
expect((firstChild as HTMLUnit).children![0].type).toBe('text');
});
});

View File

@ -0,0 +1,89 @@
import { describe, expect, it } from 'vitest';
import { parse, parseCode, parseView, wrapWithFile } from './mock';
import { types as t } from '@babel/core';
import type { ExpUnit } from '../index';
import { traverse } from '@babel/core';
describe('ExpUnit', () => {
// ---- Type
it('should identify expression unit', () => {
const viewUnits = parse('<>{count}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('exp');
});
it('should not identify literals as expression unit', () => {
const viewUnits = parse('<>{1}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).not.toBe('exp');
});
// ---- Content
it('should correctly parse content for expression unit', () => {
const viewUnits = parse('<>{count}</>');
const content = (viewUnits[0] as ExpUnit).content;
expect(t.isIdentifier(content.value, { name: 'count' })).toBeTruthy();
});
it('should correctly parse complex content for expression unit', () => {
const ast = parseCode('<>{!console.log("hello world") && myComplexFunc(count + 100)}</>');
const viewUnits = parseView(ast);
const originalExpression = (
(ast as t.JSXFragment).children[0] as t.JSXExpressionContainer
).expression;
const content = (viewUnits[0] as ExpUnit).content;
expect(content.value).toBe(originalExpression);
});
it('should correctly parse content with view prop for expression unit', () => {
// ---- <div>Ok</div> will be replaced with a random string and stored in props.viewPropMap
const viewUnits = parse('<>{<div>Ok</div>}</>');
const content = (viewUnits[0] as ExpUnit).content;
const viewPropMap = content.viewPropMap;
expect(Object.keys(viewPropMap).length).toBe(1);
const key = Object.keys(viewPropMap)[0];
const viewProp = viewPropMap[key];
// ---- Only one view unit for <div>Ok</div>
expect(viewProp.length).toBe(1);
expect(viewProp[0].type).toBe('html');
// ---- The value of the replaced prop should be the key of the viewPropMap
const value = content.value;
expect(t.isStringLiteral(value, { value: key })).toBeTruthy();
});
it('should correctly parse content with view prop for expression unit with complex expression', () => {
// ---- <div>Ok</div> will be replaced with a random string and stored in props.viewPropMap
const ast = parseCode(`<>{
someFunc(() => {
console.log("hello world")
doWhatever()
return <div>Ok</div>
})
}</>`);
const viewUnits = parseView(ast);
const content = (viewUnits[0] as ExpUnit).content;
const viewPropMap = content.viewPropMap;
expect(Object.keys(viewPropMap).length).toBe(1);
const key = Object.keys(viewPropMap)[0];
const viewProp = viewPropMap[key];
// ---- Only one view unit for <div>Ok</div>
expect(viewProp.length).toBe(1);
// ---- Check the value of the replaced prop
let idExistCount = 0;
traverse(wrapWithFile(content.value), {
StringLiteral(path) {
if (path.node.value === key) idExistCount++;
}
});
// ---- Expect the count of the id matching to be exactly 1
expect(idExistCount).toBe(1);
});
});

View File

@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
import { types as t } from '@babel/core';
import { IfUnit } from '../types';
describe('IfUnit', () => {
// ---- Type
it('should identify if unit', () => {
const viewUnits = parse('<if cond={true}>true</if>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should identify if unit with else', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else>false</else>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should identify if unit with else-if', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={false}>false</else-if>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should identify if unit with else-if and else', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={false}>false</else-if>
<else>else</else>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should identify if unit with multiple else-if', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={flag1}>flag1</else-if>
<else-if cond={flag2}>flag2</else-if>
<else>else</else>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
// ---- Branches
it('should correctly parse branches count for if unit', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={flag1}>flag1</else-if>
<else-if cond={flag2}>flag2</else-if>
<else>else</else>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches.length).toBe(4);
});
it('should correctly parse branches\' condition for if unit', () => {
const viewUnits = parse('<if cond={true}>true</if>');
const branches = (viewUnits[0] as IfUnit).branches;
expect(t.isBooleanLiteral(branches[0].condition, { value: true })).toBeTruthy();
});
it('should correctly parse branches\' children for if unit', () => {
const viewUnits = parse('<if cond={true}>true</if>');
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches[0].children.length).toBe(1);
expect(branches[0].children[0].type).toBe('text');
});
it('should correctly parse branches\' condition for if unit with else', () => {
const viewUnits = parse(`<>
<if cond={flag1}>1</if>
<else>2</else>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy();
expect(t.isBooleanLiteral(branches[1].condition, { value: true })).toBeTruthy();
});
it('should correctly parse branches\' children for if unit with else', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else>false</else>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches[0].children.length).toBe(1);
expect(branches[0].children[0].type).toBe('text');
expect(branches[1].children.length).toBe(1);
expect(branches[1].children[0].type).toBe('text');
});
it('should correctly parse branches\' condition for if unit with else-if', () => {
const viewUnits = parse(`<>
<if cond={flag1}>1</if>
<else-if cond={flag2}>2</else-if>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
/**
* () => {
* if (flag1) {
* this._prevCond
* return 1
* } else () {
* if (flag2) {
* }
* }
*/
expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy();
expect(t.isIdentifier(branches[1].condition, { name: 'flag2' })).toBeTruthy();
});
it('should correctly parse branches\' children for if unit with else-if', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={false}>false</else-if>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches[0].children.length).toBe(1);
expect(branches[0].children[0].type).toBe('text');
expect(branches[1].children.length).toBe(1);
expect(branches[1].children[0].type).toBe('text');
});
});

View File

@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
import { types as t } from '@babel/core';
import type { HTMLUnit, TemplateUnit } from '../index';
describe('TemplateUnit', () => {
// ---- Type
it('should not parse a single HTMLUnit to a TemplateUnit', () => {
const viewUnits = parse('<div></div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should parse a nested HTMLUnit to a TemplateUnit', () => {
const viewUnits = parse('<div><div></div></div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('template');
});
it('should correctly parse a nested HTMLUnit\'s structure into a template', () => {
const viewUnits = parse('<div><div></div></div>');
const template = (viewUnits[0] as TemplateUnit).template;
expect(t.isStringLiteral(template.tag, { value: 'div' })).toBeTruthy();
expect(template.children).toHaveLength(1);
const firstChild = template.children![0] as HTMLUnit;
expect(t.isStringLiteral(firstChild.tag, { value: 'div' })).toBeTruthy();
});
// ---- Props
it('should correctly parse the path of TemplateUnit\'s dynamic props in root element', () => {
const viewUnits = parse('<div class={this.name}><div></div></div>');
const dynamicProps = (viewUnits[0] as TemplateUnit).props;
expect(dynamicProps).toHaveLength(1);
const prop = dynamicProps[0];
expect(prop.path).toHaveLength(0);
});
it('should correctly parse the path of TemplateUnit\'s dynamic props in nested element', () => {
const viewUnits = parse('<div><div class={this.name}></div></div>');
const dynamicProps = (viewUnits[0] as TemplateUnit).props!;
expect(dynamicProps).toHaveLength(1);
const prop = dynamicProps[0]!;
expect(prop.path).toHaveLength(1);
expect(prop.path[0]).toBe(0);
});
it('should correctly parse the path of TemplateUnit\'s dynamic props with mutable particles ahead', () => {
const viewUnits = parse('<div><Comp/><div class={this.name}></div></div>');
const dynamicProps = (viewUnits[0] as TemplateUnit).props!;
expect(dynamicProps).toHaveLength(1);
const prop = dynamicProps[0]!;
expect(prop.path).toHaveLength(1);
expect(prop.path[0]).toBe(1);
});
it('should correctly parse the path of TemplateUnit\'s mutableUnits', () => {
const viewUnits = parse('<div><Comp/><div class={this.name}></div></div>');
const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!;
expect(mutableParticles).toHaveLength(1);
const particle = mutableParticles[0]!;
expect(particle.path).toHaveLength(1);
expect(particle.path[0]).toBe(0);
});
it('should correctly parse the path of multiple TemplateUnit\'s mutableUnits', () => {
const viewUnits = parse('<div><Comp/><div class={this.name}></div><Comp/></div>');
const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!;
expect(mutableParticles).toHaveLength(2);
const firstParticle = mutableParticles[0]!;
expect(firstParticle.path).toHaveLength(1);
expect(firstParticle.path[0]).toBe(0);
const secondParticle = mutableParticles[1]!;
expect(secondParticle.path).toHaveLength(1);
expect(secondParticle.path[0]).toBe(2);
});
});

View File

@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
import { types as t } from '@babel/core';
import type { TextUnit } from '../index';
describe('TextUnit', () => {
// ---- Type
it('should identify text unit', () => {
const viewUnits = parse('<>hello world</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with boolean expression', () => {
const viewUnits = parse('<>{true}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with number expression', () => {
const viewUnits = parse('<>{1}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with null expression', () => {
const viewUnits = parse('<>{null}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with string literal expression', () => {
const viewUnits = parse('<>{"hello world"}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
// ---- Content
it('should correctly parse content for text unit', () => {
const viewUnits = parse('<>hello world</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isStringLiteral(content, { value: 'hello world' })).toBeTruthy();
});
it('should correctly parse content for boolean text unit', () => {
const viewUnits = parse('<>{true}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isBooleanLiteral(content, { value: true })).toBeTruthy();
});
it('should correctly parse content for number text unit', () => {
const viewUnits = parse('<>{1}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isNumericLiteral(content, { value: 1 })).toBeTruthy();
});
it('should correctly parse content for null text unit', () => {
const viewUnits = parse('<>{null}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isNullLiteral(content)).toBeTruthy();
});
it('should correctly parse content for string literal text unit', () => {
const viewUnits = parse('<>{"hello world"}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isStringLiteral(content)).toBeTruthy();
});
});

View File

@ -0,0 +1 @@
declare module "@babel/plugin-syntax-jsx"

View File

@ -0,0 +1,230 @@
import babel, { parseSync, types as t } from '@babel/core';
import { AllowedJSXNode, ViewParserConfig } from '../types';
import { parseView as pV } from '..';
import babelJSX from '@babel/plugin-syntax-jsx';
const htmlTags = [
'a',
'abbr',
'address',
'area',
'article',
'aside',
'audio',
'b',
'base',
'bdi',
'bdo',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'iframe',
'img',
'input',
'ins',
'kbd',
'label',
'legend',
'li',
'link',
'main',
'map',
'mark',
'menu',
'meta',
'meter',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'picture',
'pre',
'progress',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'script',
'section',
'select',
'slot',
'small',
'source',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr',
'acronym',
'applet',
'basefont',
'bgsound',
'big',
'blink',
'center',
'dir',
'font',
'frame',
'frameset',
'isindex',
'keygen',
'listing',
'marquee',
'menuitem',
'multicol',
'nextid',
'nobr',
'noembed',
'noframes',
'param',
'plaintext',
'rb',
'rtc',
'spacer',
'strike',
'tt',
'xmp',
'animate',
'animateMotion',
'animateTransform',
'circle',
'clipPath',
'defs',
'desc',
'ellipse',
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feDropShadow',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
'filter',
'foreignObject',
'g',
'image',
'line',
'linearGradient',
'marker',
'mask',
'metadata',
'mpath',
'path',
'pattern',
'polygon',
'polyline',
'radialGradient',
'rect',
'set',
'stop',
'svg',
'switch',
'symbol',
'text',
'textPath',
'tspan',
'use',
'view',
];
export const config: ViewParserConfig = {
babelApi: babel,
htmlTags,
};
export function parseCode(code: string) {
return (
parseSync(code, {plugins: [babelJSX]})!.program
.body[0] as t.ExpressionStatement
).expression as AllowedJSXNode;
}
export function parseView(node: AllowedJSXNode) {
return pV(node, config);
}
export function parse(code: string) {
return parseView(parseCode(code));
}
export function wrapWithFile(node: t.Expression): t.File {
return t.file(t.program([t.expressionStatement(node)]));
}

View File

@ -0,0 +1,89 @@
import type Babel from '@babel/core';
import type { types as t } from '@babel/core';
export interface UnitProp {
value: t.Expression
viewPropMap: Record<string, ViewUnit[]>
specifier?: string
}
export interface TextUnit {
type: 'text'
content: t.Literal
}
export type MutableUnit = ViewUnit & { path: number[] }
export interface TemplateProp {
name: string
key: string
path: number[]
value: t.Expression
}
export interface TemplateUnit {
type: 'template'
template: HTMLUnit
mutableUnits: MutableUnit[]
props: TemplateProp[]
}
export interface HTMLUnit {
type: 'html'
tag: t.Expression
content?: UnitProp
props?: Record<string, UnitProp>
children?: ViewUnit[]
}
export interface CompUnit {
type: 'comp'
tag: t.Expression
content?: UnitProp
props?: Record<string, UnitProp>
children?: ViewUnit[]
}
export interface IfBranch {
condition: t.Expression
children: ViewUnit[]
}
export interface IfUnit {
type: 'if'
branches: IfBranch[]
}
export interface ExpUnit {
type: 'exp'
content: UnitProp
}
export interface EnvUnit {
type: 'env'
props: Record<string, UnitProp>
children: ViewUnit[]
}
export type ViewUnit =
| TextUnit
| HTMLUnit
| CompUnit
| IfUnit
| ExpUnit
| EnvUnit
| TemplateUnit
export interface ViewParserConfig {
babelApi: typeof Babel
htmlTags: string[]
parseTemplate?: boolean
}
export type AllowedJSXNode =
| t.JSXElement
| t.JSXFragment
| t.JSXText
| t.JSXExpressionContainer
| t.JSXSpreadChild

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -1,3 +1,4 @@
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
- 'packages/**/*'