Signed-off-by: Avior <git@avior.me>
This commit is contained in:
2025-06-30 22:41:18 +02:00
commit 86b37af716
16 changed files with 1526 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
**.transformed.ts

60
Button.processed.ts Normal file
View File

@ -0,0 +1,60 @@
import { attribute, Component, WebElement, cls, html } from '..';
@Component('butt-on')
export default class Button extends WebElement {
@attribute('boolean')
public block?: boolean;
@attribute()
public iconLeft?: any;
@attribute()
public iconRight?: any;
@attribute('boolean')
public outline?: boolean;
@attribute('boolean')
public outlineR?: boolean;
@attribute('boolean')
public outlineG?: boolean;
@attribute('boolean')
public ghost?: boolean;
@attribute('boolean')
public disabled?: boolean | undefined;
@attribute()
public name?: string;
@attribute()
public value?: string;
@attribute()
public tag?: string;
@attribute()
public enctype?: string;
@attribute()
public class?: string;
@attribute()
public href?: string;
public override async render() {
const classes = [
'button',
'no-link-style',
'focus:ring',
{ 'w-full': this.block },
{ outline: this.outline },
{ outlineR: this.outlineR && !this.disabled },
{ outlineG: this.outlineG && !this.disabled },
{ ghost: this.ghost },
{ disabled: this.disabled },
this.class,
];
const tag = this.tag ?? this.href ? 'a' : 'button';
return html `
<${tag} ${{ ...this.getProps() }} class="${cls(classes)}">
${this.iconLeft}
<slot />
${this.iconRight}
</${tag}>
`;
}
private onClick = () => {
if (this.disabled) {
return;
}
console.log('Button clicked');
};
}

218
bun.lock Normal file
View File

@ -0,0 +1,218 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"@dzeio/object-util": "^1.9.1",
"jscodeshift": "^17.3.0",
"typescript": "^5.8.3",
},
"devDependencies": {
"@types/jscodeshift": "^17.3.0",
},
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.27.7", "", {}, "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ=="],
"@babel/core": ["@babel/core@7.27.7", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w=="],
"@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
"@babel/parser": ["@babel/parser@7.27.7", "", { "dependencies": { "@babel/types": "^7.27.7" }, "bin": "./bin/babel-parser.js" }, "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q=="],
"@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="],
"@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-flow": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg=="],
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="],
"@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="],
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="],
"@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="],
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg=="],
"@babel/preset-flow": ["@babel/preset-flow@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-flow-strip-types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg=="],
"@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
"@babel/register": ["@babel/register@7.27.1", "", { "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-K13lQpoV54LATKkzBpBAEu1GGSIRzxR9f4IN4V8DCDgiUMo2UDGagEZr3lPeVNJPLkWUi5JE4hCHKneVTwQlYQ=="],
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.27.7", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", "@babel/types": "^7.27.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw=="],
"@babel/types": ["@babel/types@7.27.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw=="],
"@dzeio/object-util": ["@dzeio/object-util@1.9.1", "", {}, "sha512-cLGsjAc7hzSadS57jcMxSPidYabyZXJOFnasScSrE/V5yflhze6T7L5/98josWYrXMvoKu7N+Ivk6vGkIj72UQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.3", "", {}, "sha512-AiR5uKpFxP3PjO4R19kQGIMwxyRyPuXmKEEy301V1C0+1rVjS94EZQXf1QKZYN8Q0YM+estSPhmx5JwNftv6nw=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.28", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-KNNHHwW3EIp4EDYOvYFGyIFfx36R2dNJYH4knnZlF8T5jdbD5Wx8xmSaQ2gP9URkJ04LGEtlcCtwArKcmFcwKw=="],
"@types/jscodeshift": ["@types/jscodeshift@17.3.0", "", { "dependencies": { "ast-types": "^0.16.1", "recast": "^0.23.11" } }, "sha512-ogvGG8VQQqAQQ096uRh+d6tBHrYuZjsumHirKtvBa5qEyTMN3IQJ7apo+sw9lxaB/iKWIhbbLlF3zmAWk9XQIg=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="],
"clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.178", "", {}, "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-cache-dir": ["find-cache-dir@2.1.0", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" } }, "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ=="],
"find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
"flow-parser": ["flow-parser@0.274.2", "", {}, "sha512-kCjoA1h5j+Ttu/9fekY9XzeKPG8SvNtxigiCkezmDIOlcKr+d9LysczrPylEeSYINE3sLlX45W5vT2CroD6sWA=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
"isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jscodeshift": ["jscodeshift@17.3.0", "", { "dependencies": { "@babel/core": "^7.24.7", "@babel/parser": "^7.24.7", "@babel/plugin-transform-class-properties": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/preset-flow": "^7.24.7", "@babel/preset-typescript": "^7.24.7", "@babel/register": "^7.24.6", "flow-parser": "0.*", "graceful-fs": "^4.2.4", "micromatch": "^4.0.7", "neo-async": "^2.5.0", "picocolors": "^1.0.1", "recast": "^0.23.11", "tmp": "^0.2.3", "write-file-atomic": "^5.0.1" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" }, "optionalPeers": ["@babel/preset-env"], "bin": { "jscodeshift": "bin/jscodeshift.js" } }, "sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkg-dir": ["pkg-dir@3.0.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
}
}

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"dependencies": {
"@dzeio/object-util": "^1.9.1",
"jscodeshift": "^17.3.0",
"typescript": "^5.8.3"
},
"devDependencies": {
"@types/jscodeshift": "^17.3.0"
}
}

161
src/compiler/codeshift.ts Normal file
View File

@ -0,0 +1,161 @@
import { ArrayExpression, Identifier, JSCodeshift, Literal, ObjectExpression, Property, Transform } from "jscodeshift"
import pathUtils from 'path/posix'
interface ObjectField {
type: 'Object'
items: Record<string, Field>
item: ObjectExpression
}
interface EndField {
type: 'Literal'
item: Literal
}
interface ArrayField {
type: 'Array'
items: Array<Field>
item: ArrayExpression
}
type Field = ObjectField | EndField | ArrayField
type Possible = ObjectExpression | ArrayExpression | Literal
function processItem(value: Possible): Field {
if (value.type === 'ObjectExpression') {
return simplify(value)
} else if (value.type === 'ArrayExpression') {
const field: Field = {
type: 'Array',
items: [],
item: value
}
value.elements.forEach((it) => {
field.items.push(processItem(it as Possible))
})
return field
} else {
return {
type: 'Literal',
item: value
}
}
}
function simplify(base: ObjectExpression): ObjectField {
const list: ObjectField['items'] = {}
base.properties.forEach((it) => {
const item = it as Property
const key = (item.key as Identifier).name
list[key] = processItem(item.value as Possible)
})
return {
type: 'Object',
items: list,
item: base
}
}
function exists(path: ObjectExpression | ArrayExpression, key: string | number) {
if (path.type === 'ObjectExpression') {
path.properties.forEach((p) => {
const prop = p as Property
if ((prop.key as Identifier).name === (key + '')) {
return true
}
})
return false
} else {
}
}
function set(j: JSCodeshift, path: ObjectExpression | ArrayExpression, value: Possible, key: string | number, options?: { override?: boolean }) {
let exists = false
if (path.type === 'ObjectExpression') {
path.properties.forEach((p) => {
const prop = p as Property
if ((prop.key as Identifier).name === (key + '')) {
exists = true
if (!options?.override) {
console.warn('Property already exist, add the option override to change it')
return
}
prop.value = value
}
})
if (exists) { return }
if (key.toString().includes('-')) {
key = `'${key.toString()}'`
}
path.properties.push(j.property('init', j.identifier(key + ''), value))
} else {
}
}
function remove(path: ObjectExpression | ArrayExpression, key: string | number) {
if (path.type === 'ObjectExpression') {
const index = path.properties.findIndex((p) => ((p as Property).key as Identifier).name === (key + ''))
if (index === -1) {
return
}
path.properties.splice(index)
} else {
}
}
function rename(parent: ObjectExpression, oldKey: string, newKey: string) {
parent.properties.forEach((p) => {
if (p.key.name === oldKey) {
p.key.name = newKey
}
})
}
/**
* Start editing here !
*/
module.exports = (file, api): Transformer => {
const j = api.jscodeshift
const root = j(file.source)
return root
.find(j.ObjectExpression)
.forEach((path, index) => {
if (index !== 0) return
const filename = pathUtils.basename(file.path, '.ts')
let simplified = simplify(path.node)
rename(simplified.item, 'abbrevation', 'abbreviations')
// set(j, simplified.item, j.objectExpression([j.property('init', j.identifier('fr'), j.literal(abbr))]), 'abbrevation')
// set(j, simplified.item, j.literal('a'), 's.official')
// Example remove field
// remove(name.item as ObjectExpression, 'fr')
// Example Set/Add regulationMArk to cards
// set(j, name.items.fr, j.literal('D'), 'regulationMark')
// console.log(filename)
const ids = [
5, 6, 8, 11, 12, 13, 14, 17, 22, 23, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 37, 41, 43, 44, 45, 46, 49, 51, 54, 56, 57, 58, 59, 60, 64, 65, 70, 73, 75, 76, 78, 80, 82, 91, 92, 116, 117, 119, 128, 129
]
const id = parseInt(filename)
const isHolo = ids.includes(id) || id >= 131
const isNormal = !isHolo
if (isHolo) {
set(j, simplified.items.variants.item as ObjectExpression, j.literal(true), 'holo')
set(j, simplified.items.variants.item as ObjectExpression, j.literal(false), 'normal')
} else {
remove(simplified.item, 'variants')
}
})
.toSource({ useTabs: true, lineTerminator: '\n' }).replace(/ /g, ' ')
}
module.exports.parser = 'ts'

220
src/compiler/index.ts Normal file
View File

@ -0,0 +1,220 @@
import ts from 'typescript'
import fs from 'fs'
const input = 'src/components/button/index.ts' // Input file path
const output = 'src/components/button/Button.transformed.ts' // Output file path for transformed code
interface SimpleHTMLElement {
name: string
attrs?: Record<string, string | any>
childs?: Array<SimpleHTMLElement | string>
}
// Read input source code
const src = fs.readFileSync(input, 'utf-8')
// Create a TypeScript source file from input source code string
const sourceFile = ts.createSourceFile(input, src, ts.ScriptTarget.Latest, true)
function parseName(name: string, values: Array<ts.Expression>): string | ts.Expression {
const match = /^\u0000(\d+)\u0000$/.exec(name)
if (match) {
const idx = Number(match[1])
return values[idx]
}
return name
}
function parseAttrs(attrString: string, values: Array<ts.Expression>): Record<string, string | ts.Expression> {
const attrs: Record<string, string | ts.Expression> = {}
console.log(attrString) // \u00001\u0000
const parts = attrString.match(/(?:[^.*=]+=(?:"[^"]*"|'[^']*'|[^.*"']+))/g) ?? []
for (const part of parts) {
if (!part) {
continue
}
const [key, raw] = part.trim().split('=') as [string, string]
if (!raw) {
continue
}
const val = raw.replace(/^['"]?|['"]?$/g, '')
const match = /^\u0000(\d+)\u0000$/.exec(val)
if (match) {
const idx = Number(match[1])
attrs[key] = values[idx]
} else {
attrs[key] = val
}
}
return attrs
}
/**
* Naively parse a TemplateLiteral assumed to contain a simple html tagged template string.
* Extracts:
* - tagName (e.g. 'button')
* - attrs as key-value pairs (only "class" attribute parsed here)
* - children: a mix of static strings and interpolated expressions
* @param template TemplateLiteral node from the AST (expects TemplateExpression)
* @returns parsed SimpleHTMLElement parts or null if parsing fails
*/
function extractHtmlData(template: ts.TemplateLiteral): {
tagName: string | ts.Expression
attrs: Record<string, string | ts.Expression>
children: (ts.Expression | ts.StringLiteral)[]
} | null {
// We only handle TemplateExpression (with interpolations) here
if (!ts.isTemplateExpression(template)) return null
// Collect all template literal strings and expressions separately
const fullStrings: string[] = [template.head.text]
const exprs: ts.Expression[] = []
// Each template span has an expression and a literal text after it
let idx = 0
for (const span of template.templateSpans) {
exprs.push(span.expression) // interpolation `${...}`
fullStrings.push(`\u0000${idx++}\u0000`) // literal after interpolation
fullStrings.push(span.literal.text) // literal after interpolation
}
console.log('aaa', exprs, fullStrings.join('').replaceAll(/\s*\n\s*/g, '').trim())
// Join all literal parts to parse the opening tag naively with regex
const fullHtml = fullStrings.join('').replaceAll(/\s*\n\s*/g, '').trim()
const selfClose = /^<([a-zA-Z0-9-\u0000]+)([^>]*)\/>$/.exec(fullHtml)
if (selfClose) {
const [, tagName, rawAttrs] = selfClose
const attrs = parseAttrs(rawAttrs, exprs)
return {
tagName: parseName(tagName, exprs),
attrs: Object.keys(attrs).length ? attrs : {},
children: []
}
}
const fullMatch = /^<([a-zA-Z0-9-\u0000]+)([^>]*)>([\s\S]*)<\/.*>$/.exec(fullHtml)
if (!fullMatch) {
throw new Error(`Invalid HTML: ${fullHtml}`)
}
const [, tagName, rawAttrs, inner] = fullMatch
const attrs = parseAttrs(rawAttrs, exprs)
const children: (ts.Expression | ts.StringLiteral)[] = []
const startsWithShit = inner.startsWith('\u0000')
inner.split(/\u0000/).forEach((part, i) => {
if (i % 2 === (startsWithShit ? 1 : 0) && exprs[Number.parseInt(part)]) {
children.push(exprs[Number.parseInt(part)])
} else if (part) {
children.push(ts.factory.createStringLiteral(part))
}
})
// // Build children array alternating strings and expressions from template parts
// const children: (ts.Expression | ts.StringLiteral)[] = []
// for (let i = 0; i < fullStrings.length; i++) {
// console.log(fullStrings[i], exprs[i])
// // Add string chunk if not empty
// if (inner[i]) {
// children.push(ts.factory.createStringLiteral(fullStrings[i]))
// }
// // Add expression if exists (one less than strings length)
// if (exprs[i]) {
// children.push(exprs[i])
// }
// }
return {
tagName: parseName(tagName, exprs),
attrs,
children,
}
}
/**
* TypeScript Transformer Factory
* Transforms `html` tagged template literals into SimpleHTMLElement object literals.
* Only handles simple cases with static tags and attributes and interpolated children.
*/
const transformer: ts.TransformerFactory<ts.SourceFile> = context => {
return rootNode => {
// Recursive AST visitor function
function visit(node: ts.Node): ts.Node {
// Check if node is a tagged template expression with tag name 'html'
if (
ts.isTaggedTemplateExpression(node) &&
node.tag.getText() === 'html' &&
ts.isTemplateExpression(node.template)
) {
// Extract html data from template literal
const parsed = extractHtmlData(node.template)
// console.log(parsed)
if (!parsed) return node // fallback: no transform if parsing failed
// Create AST properties for the SimpleHTMLElement object literal
// console.log(parsed.tagName)
const props: ts.ObjectLiteralElementLike[] = [
// name property: tag name as string literal
ts.factory.createPropertyAssignment(
'name',
typeof parsed.tagName === 'string' ? ts.factory.createStringLiteral(parsed.tagName) : parsed.tagName
),
]
// Add attrs property if any attributes found
if (Object.keys(parsed.attrs).length > 0) {
props.push(
ts.factory.createPropertyAssignment(
'attrs',
ts.factory.createObjectLiteralExpression(
// Create key-value pairs for each attribute
Object.entries(parsed.attrs).map(([k, v]) =>
ts.factory.createPropertyAssignment(k, typeof v === 'string' ? ts.factory.createStringLiteral(v) : v)
),
true // multiline formatting
)
)
)
}
// Add childs property if any children (strings or expressions)
if (parsed.children.length > 0) {
props.push(
ts.factory.createPropertyAssignment(
'childs',
ts.factory.createArrayLiteralExpression(parsed.children, true)
)
)
}
// Return the object literal AST node that replaces the `html` tagged template call
return ts.factory.createObjectLiteralExpression(props, true)
}
// Recursively visit children nodes
return ts.visitEachChild(node, visit, context)
}
// Start AST traversal from root
return ts.visitNode(rootNode, visit) as ts.SourceFile
}
}
// Run the transform on the source file with our custom transformer
const result = ts.transform(sourceFile, [transformer])
// Prepare printer to output transformed AST back to TypeScript code string
const printer = ts.createPrinter()
// Get transformed source code text
const outputText = printer.printFile(result.transformed[0])
// Write the transformed source code to output file
fs.writeFileSync(output, outputText)
console.log(`✅ Wrote transformed file to: ${output}`)

View File

@ -0,0 +1,5 @@
---
import Item from '.'
import AstroSSR from '../utils/AstroSSR.astro'
---
<AstroSSR component={Item} props={Astro.props} slots={Astro.slots} />

View File

@ -0,0 +1,22 @@
import { Component, WebElement, type SimpleHTMLElement, html, cls, attribute } from '..'
@Component('bad-ge')
export default class Badge extends WebElement {
@attribute()
public badge?: WebElement
@attribute()
public class?: string
public override async render(): Promise<SimpleHTMLElement> {
return html`
<div class=${cls(['px-4 py-1 text-center rounded-full relative flex', this.class])}>
<div class="overflow-ellipsis w-full overflow-clip">
${this.badge}
<slot/>
</div>
</div>
`
}
}

View File

@ -0,0 +1,5 @@
---
import Btn from '.'
import AstroSSR from '../utils/AstroSSR.astro'
---
<AstroSSR component={Btn} props={Astro.props} slots={Astro.slots} />

View File

@ -0,0 +1,74 @@
import { attribute, Component, WebElement, cls, html } from '..'
@Component('butt-on')
export default class Button extends WebElement {
@attribute('boolean')
public block?: boolean
@attribute()
public iconLeft?: any
@attribute()
public iconRight?: any
@attribute('boolean')
public outline?: boolean
@attribute('boolean')
public outlineR?: boolean
@attribute('boolean')
public outlineG?: boolean
@attribute('boolean')
public ghost?: boolean
@attribute('boolean')
public disabled?: boolean | undefined
@attribute()
public name?: string
@attribute()
public value?: string
@attribute()
public tag?: string
@attribute()
public enctype?: string
@attribute()
public class?: string
@attribute()
public href?: string
public override async render() {
const classes = [
'button',
'no-link-style',
'focus:ring',
{ 'w-full': this.block },
{ outline: this.outline },
{ outlineR: this.outlineR && !this.disabled },
{ outlineG: this.outlineG && !this.disabled },
{ ghost: this.ghost },
{ disabled: this.disabled },
this.class,
]
const tag = this.tag ?? this.href ? 'a' : 'button'
return html`
<${tag} ${{ ...this.getProps() }} class="${cls(classes)}">
${this.iconLeft}
<slot />
${this.iconRight}
</${tag}>
`
}
private onClick = () => {
if (this.disabled) { return }
console.log('Button clicked')
}
}

7
src/components/index.ts Normal file
View File

@ -0,0 +1,7 @@
import WebElement from './utils/web-element'
export {
WebElement
}
export * from './utils/decorators'
export * from './utils/utils'

11
src/components/list.ts Normal file
View File

@ -0,0 +1,11 @@
import Button from './button'
import Badge from './badge'
const components = [
Button,
Badge
]
export function loadComponents() {
return components
}

View File

@ -0,0 +1,32 @@
---
import { objectRemap, objectSize } from '@dzeio/object-util'
import { html, type SimpleHTMLElement, type WebElement } from '..'
export interface Props {
component: new () => WebElement
props?: Record<string, any>
slots?: typeof Astro.slots
}
const omp = new Astro.props.component()
const Tag = omp.getConfig().tag
// parse Astro slots
const slots : Record<string, string | SimpleHTMLElement> = {}
for (const slot of Object.keys(Astro.slots)) {
slots[slot] = html(await Astro.slots.render(slot))
}
if (Astro.props.slots) {
for (const slot of Object.keys(Astro.props.slots)) {
slots[slot] = html(await Astro.props.slots.render(slot))
}
}
// render the component server-side
omp.setProps(Astro.props.props)
omp.setSlots(slots)
const rendered = await omp
.renderString()
---
<Tag set:html={rendered.output} data-hydrate={rendered.needHydration ? true : undefined} {...omp.getProps()} data-slots={objectSize(slots) > 0 ? JSON.stringify(slots) : undefined} />

View File

@ -0,0 +1,121 @@
import { WebElement } from '..'
interface ComponentConfig {
tag: `${string}-${string}`
attrs?: Record<string, AttributeOptions>
shadow?: boolean
}
/**
* Decorator to define a custom element.
* @param tagOrConfig The tag name of the custom element. it MUST respect the formatting else the browser will not recognize it.
*/
export function Component(tagOrConfig: ComponentConfig['tag'] | Omit<ComponentConfig, 'attrs'>) {
const conf = typeof tagOrConfig === 'string' ? { tag: tagOrConfig } : tagOrConfig
return function(constructor: typeof WebElement) {
const child = class extends constructor {
// public static override readonly tag = tag
public constructor() {
super()
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const proto = ((this as any).__proto__ as typeof WebElement)
Object.assign(proto.__config, conf)
proto.__config.tag = conf.tag
const config = proto.__config
for (const attr of Object.keys(config.attrs ?? {})) {
const pouet = config.attrs![attr]
// console.log(attr, pouet)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete proto[attr as keyof typeof WebElement]
lateAttribute(this, attr, pouet)
}
}
}
if (typeof customElements === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return child as any
}
if (!customElements.get(conf.tag)) {
customElements.define(conf.tag, child)
} else {
throw new Error(`Custom element ${conf.tag} is already defined`)
}
}
}
export interface AttributeOptions {
type?: 'string' | 'number' | 'boolean'
onChange?: (newValue: any, oldValue: any) => void
}
export function lateAttribute(el: WebElement, name: string, options: AttributeOptions = {}) {
if (!(el instanceof WebElement)) {
return
}
const privateKey = `__${name}`
Object.defineProperty(el, name, {
get(this: WebElement) {
return this[privateKey as keyof WebElement]
},
set(this: WebElement, newVal) {
const oldVal = this[privateKey as keyof WebElement]
const changed = newVal !== oldVal
if (!changed) {
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
(this as any)[privateKey] = newVal
options.onChange?.(newVal, oldVal)
if (newVal == null || newVal === false) {
this.removeAttribute(name)
} else {
this.setAttribute(name, String(newVal))
}
// invalide current rendering
this.invalidate()
// trigger new render
void this.triggerRender()
},
configurable: true,
enumerable: true,
})
}
type AttributeFunction = (proto: any, name: string) => void
/**
* Decorator to define an attribute on a WebElement.
*/
export function attribute(): AttributeFunction
/**
* Decorator to define an attribute on a WebElement.
* @param type The type of the attribute.
*/
// eslint-disable-next-line @typescript-eslint/unified-signatures
export function attribute(type: AttributeOptions['type']): AttributeFunction
/**
* Decorator to define an attribute on a WebElement.
* @param options The options for the attribute.
*/
// eslint-disable-next-line @typescript-eslint/unified-signatures
export function attribute(options: AttributeOptions): AttributeFunction
export function attribute(typeOrOptions?: AttributeOptions['type'] | AttributeOptions): AttributeFunction {
return function(proto: typeof WebElement, name: string) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
proto.__config ??= {} as unknown as typeof proto['__config']
proto.__config.attrs ??= {}
proto.__config.attrs[name] = typeof typeOrOptions === 'string' ? { type: typeOrOptions } : typeOrOptions ?? {}
}
}

View File

@ -0,0 +1,291 @@
/* eslint-disable max-depth */
/* eslint-disable complexity */
import { objectMap } from '@dzeio/object-util'
import { WebElement } from '..'
export function HTMLtoString(tag: SimpleHTMLElement): string {
// compile attributes (note: the attributes values are encoded to not break json)
const attrs = objectMap(tag.attrs ?? {}, (value, key) => ` ${key}="${value.replaceAll('"', '&#34;')}"`).join('')
// console.log('a', tag)
// console.log('a', tag.childs)
let childs = ''
if (tag.childs) {
console.log('na', tag, tag.childs)
childs = tag.childs.map((it) => typeof it === 'string' ? it : HTMLtoString(it)).join('')
}
return `<${tag.name}${attrs}>${childs}</${tag.name}>`
}
export function HTMLToElement(tag: SimpleHTMLElement): HTMLElement {
// create root element
const el = document.createElement(tag.name)
// apply attributes
if (tag.attrs) {
for (const [k, v] of Object.entries(tag.attrs)) {
el.setAttribute(k, v)
}
}
// apply event handlers
if (tag.events) {
for (const [evt, handler] of Object.entries(tag.events)) {
el.addEventListener(evt, handler)
}
}
// apply childs
if (tag.childs) {
for (const child of tag.childs) {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child))
} else {
el.appendChild(HTMLToElement(child))
}
}
}
return el
}
export interface SimpleHTMLElement {
name: string
attrs?: Record<string, string>
events?: Record<string, EventListener>
childs?: Array<SimpleHTMLElement | string>
}
export function hasEvents(el: SimpleHTMLElement): boolean {
if (Object.keys(el.events ?? {}).length > 0) {
return true
}
if (el.childs) {
for (const child of el.childs) {
if (typeof child === 'string') {
continue
}
if (hasEvents(child)) {
return true
}
}
}
return false
}
function parseAttrs(attrString: string, values: Array<unknown>): { attrs: Record<string, string>, events: Record<string, EventListener> } {
const attrs: Record<string, string> = {}
const events: Record<string, EventListener> = {}
const parts = attrString.match(/(?:[^.*=]+=(?:"[^"]*"|'[^']*'|[^.*"']+))/g) ?? []
for (const part of parts) {
if (!part) {
continue
}
const [key, raw] = part.trim().split('=') as [string, string]
if (!raw) {
continue
}
const val = raw.replace(/^['"]?|['"]?$/g, '')
const match = /^\u0000(\d+)\u0000$/.exec(val)
if (match) {
const idx = Number(match[1])
const value = values[idx]
if (key.startsWith('on') && typeof value === 'function') {
events[key.slice(2)] = value as EventListener
} else if (typeof value === 'string') {
attrs[key] = value
}
} else {
attrs[key] = val
}
}
return { attrs, events }
}
// TODO: rework
function parseElement(fragment: string, values: Array<unknown> = []): SimpleHTMLElement | string {
fragment = fragment.trim()
if (!fragment.startsWith('<')) { return fragment }
// self-closing tag
const selfClose = /^<([a-zA-Z0-9_-]+)([^>]*)\/>$/.exec(fragment)
if (selfClose) {
const [, tagName, rawAttrs] = selfClose
const { attrs, events } = parseAttrs(rawAttrs, values)
return {
name: tagName,
attrs: Object.keys(attrs).length ? attrs : undefined,
events: Object.keys(events).length ? events : undefined,
}
}
// regular tag
const fullMatch = /^<([a-zA-Z0-9_-]+)([^>]*)>([\s\S]*)<\/\1>$/.exec(fragment)
if (!fullMatch) { return fragment }
const [, tagName, rawAttrs, inner] = fullMatch
const { attrs, events } = parseAttrs(rawAttrs, values)
const childs: Array<SimpleHTMLElement | string> = []
let rest = inner.trim()
while (rest) {
// interpolation placeholder
const interp = /^\u0000(\d+)\u0000/.exec(rest)
if (interp) {
const idx = Number(interp[1])
const val = values[idx]
if (val instanceof WebElement) {
// childs.push(val.render())
} else if (typeof val === 'object' && val !== null && 'name' in val) { childs.push(val as SimpleHTMLElement) }
else { childs.push(String(val)) }
rest = rest.slice(interp[0].length).trim()
continue
}
if (rest.startsWith('<')) {
const tagMatch = /^<([a-zA-Z0-9_-]+)/.exec(rest)
if (!tagMatch) { break }
const childTag = tagMatch[1]
let depth = 0
let i = 0
for (; i < rest.length; i++) {
if (rest.startsWith(`<${childTag}`, i)) { depth++ }
else if (rest.startsWith(`</${childTag}>`, i)) {
depth--
if (depth === 0) {
i += (`</${childTag}>`).length
break
}
}
}
const chunk = rest.slice(0, i)
childs.push(parseElement(chunk, values) as SimpleHTMLElement)
rest = rest.slice(i).trim()
} else {
const nextIndices = [rest.indexOf('<'), rest.indexOf('\u0000')].filter((i) => i >= 0)
const idx = nextIndices.length ? Math.min(...nextIndices) : -1
const text = idx >= 0 ? rest.slice(0, idx) : rest
childs.push(text.trim())
rest = idx >= 0 ? rest.slice(idx).trim() : ''
}
}
return {
name: tagName,
attrs: Object.keys(attrs).length ? attrs : undefined,
events: Object.keys(events).length ? events : undefined,
childs: childs.length ? childs : undefined,
}
}
/**
* Parses a tagged template literal into a SimpleHTMLElement.
*/
export function html(str: string): SimpleHTMLElement
export function html(strings: TemplateStringsArray, ...values: Array<string | number | undefined | boolean | object | null | WebElement>): SimpleHTMLElement
export function html(strings: TemplateStringsArray | string, ...values: Array<unknown>): SimpleHTMLElement {
// input as a raw string, limited parsing.
if (typeof strings === 'string') {
return parseElement(strings.replaceAll('&#34;', '"')) as SimpleHTMLElement
}
// the new built string
let full = ''
// indexes of values that are already parsed into the `full` string
for (let idx = 0; idx < strings.length; idx++) {
full += strings[idx]!
if (idx < values.length) {
const value = values[idx]
switch (typeof value) {
case 'undefined': {
break
}
// if the value is a string, add it to the full string and mark it for removal
case 'string': {
full += values[idx] as string
break
}
case 'object': {
if (value === null) {
break
}
if (value instanceof WebElement) {
} else {
const attrs = Object.entries(value as Record<string, unknown>)
.filter(([, value]) => typeof value !== 'undefined' && value !== null)
.map(([key, value]) => `${key}="${(value as string).replace('"', '&#34;')}"`)
.join(' ')
full += attrs
break
}
}
default: {
// add a placeholder for the value to be parsed later
full += `\u0000${idx}\u0000`
}
}
}
}
// parse & return :D
return parseElement(full.replaceAll('&#34;', '"'), values) as SimpleHTMLElement
}
type ClassList = Array<string | Record<string, any>>
/**
* Simple helper function to create a string with class names.
* @param items - Array of class names or objects with class names as keys and boolean values as values.
* @returns A string with the class names separated by spaces.
*/
export function cls(items: string | ClassList): string
/**
* Simple helper function to create a string with class names.
* @param items - Array of class names or objects with class names as keys and boolean values as values.
* @returns A string with the class names separated by spaces.
*/
export function cls(...items: ClassList | Array<ClassList>): string
/**
* Simple helper function to create a string with class names.
* @param items - Array of class names or objects with class names as keys and boolean values as values.
* @returns A string with the class names separated by spaces.
*/
export function cls(...items: Array<string | undefined | null | Record<string, any>>): string {
if (items.length === 1 && typeof items[0] === 'string') {
return items[0]
} else if (items.length === 1 && Array.isArray(items[0])) {
items = items[0] as ClassList
}
return items.map((item) => {
if (typeof item === 'undefined' || item === null) {
return null
}
if (typeof item === 'string') {
return item
}
return Object.keys(item).filter((key) => item[key]).join(' ')
}).filter((it) => !!it).join(' ')
}
export function attrs(string: Record<string, any>): string {
return ''
}
export function getContext(): 'browser' | 'node' {
if (typeof document !== 'undefined') {
return 'browser'
}
return 'node'
}
export function assert(bool: any, message?: string): asserts bool {
if (!bool) {
throw new Error(message ?? 'Assertion failed')
}
}

View File

@ -0,0 +1,287 @@
/* eslint-disable max-classes-per-file */
import { objectLoop } from '@dzeio/object-util'
import { assert, type AttributeOptions, html, type SimpleHTMLElement, HTMLToElement, HTMLtoString, getContext, hasEvents } from '..'
// Polyfill HTMLElement on server to allow SSR rendering
let localHTMLElement: typeof HTMLElement
if (typeof HTMLElement !== 'undefined') {
localHTMLElement = HTMLElement
} else {
// @ts-expect-error polyfill for SSR
localHTMLElement = class HTMLElement {
public attachShadow(_options: ShadowRootInit): ShadowRoot {
return undefined as unknown as ShadowRoot
}
public setAttribute(_name: string, _value: string) {
// super.setAttribute(name, value)
}
}
}
/**
* Wrapper around HTMLElement that provides a simple way to create custom elements.
*/
export default class WebElement extends localHTMLElement {
public static readonly tag: string = 'web-element'
public static __config: {
tag: string
attrs?: Record<string, AttributeOptions>
} = {
tag: 'web-element'
}
/**
* childs of the current element that will be rendered inside `<slot />` elements
*/
public childs: Record<string, SimpleHTMLElement | string> = {}
/**
* Indicates whether the element has been mounted in the DOM.
*/
private mounted = false
/**
* Indicates whether the element needs to be rendered.
*/
private needRender = true
/**
* The last rendered element for caching purposes.
*/
private lastRender: SimpleHTMLElement | undefined
public constructor() {
super()
// attach to shadow root
// this.attachShadow({ mode: 'open' })
// this.connectedCallback()
}
public static async ref(props?: any): Promise<SimpleHTMLElement> {
const conf = this.__config
const self = new this()
.setProps(props)
return html`<${conf.tag}>${(await self.renderString()).output}</${conf.tag}>`
}
public getConfig(): typeof WebElement['__config'] {
return this.__proto__.__config
}
/**
* Function run when the element is connected to the DOM.
*/
public connectedCallback() {
// setup slots
if (this.dataset.slots) {
this.setSlots(JSON.parse(this.dataset.slots) as Record<string, SimpleHTMLElement>)
this.removeAttribute('data-slots')
}
// move the element inside of the shadow root
// this.shadowRoot!.innerHTML = this.innerHTML
// this.innerHTML = ''
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const attrSettings = ((this as any).__proto__ as typeof WebElement).__config.attrs
for (const key of Object.keys(attrSettings)) {
if (this.hasAttribute(key)) {
this.setProps({[key]: this.getAttribute(key)})
}
}
// setup attributes observer
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
const attr = mutation.attributeName
if (mutation.type === 'attributes' && attr) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const attrSettings = ((this as any).__proto__ as typeof WebElement).__config.attrs?.[attr]
if (!attrSettings) {
return
}
const type = attrSettings.type ?? 'string'
switch (type) {
case 'boolean': {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(this as any)[attr] = this.hasAttribute(attr) ? this.getAttribute(attr) !== 'false' : false
break
}
case 'number': {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(this as any)[attr] = this.hasAttribute(attr) ? parseInt(this.getAttribute(attr) ?? '0', 10) : undefined
break
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(this as any)[attr] = this.getAttribute(attr)
}
}
}
})
})
observer.observe(this, {
attributes: true
})
// trigger frontend hydration
if (this.dataset.hydrate === 'true') {
this.removeAttribute('data-hydrate')
void this.triggerRender()
}
}
/**
* Called when the component is mounted in the DOM
*/
public didMount() { /** child to implement */}
/**
* Called when the component was updated from an attribute change
*/
public didUpdate() { /** child to implement */}
/**
* Render the component, this MUST be stateless and return the result of the HTML `html` helper
*
* note: if a parent changes, this render function won't be run again
*/
public async render(): Promise<SimpleHTMLElement> {
return html`<div />`
}
/**
* Invalidate the component, this will trigger a re-render on the next call to `triggerRender`
*/
public invalidate() {
this.needRender = true
}
public setSlots(name: string, value: SimpleHTMLElement | string | null): this
public setSlots(slots: Record<string, SimpleHTMLElement | string | null>): this
public setSlots(slots: Record<string, SimpleHTMLElement | string | null> | string, element?: SimpleHTMLElement | string | null) {
if (typeof slots === 'string') {
slots = { [slots]: element as SimpleHTMLElement | string | null }
}
objectLoop(slots, (value, key) => {
if (value === null) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.childs[key]
} else {
this.childs[key] = value
}
})
return this
}
public getProps(): Record<string, any> {
const props: Record<string, any> = {}
for (const key of Object.keys(this.getConfig().attrs ?? {})) {
props[key] = this[key]
}
return props
}
public setProps(props?: Partial<Record<string, any>>) {
if (!props) {return this}
objectLoop(props, (value, key) => {
this[key] = value
})
return this
}
/**
* Render the component to a string.
*
* warning: events are excluded from the output.
*/
public async renderString(): Promise<{output: string, needHydration: boolean}> {
const res = await this.renderOrCache()
// SSR: manually replace <slot> tags
let out = HTMLtoString(res)
for (const [name, el] of Object.entries(this.childs)) {
let regex: RegExp
if (name === 'default') {
regex = /<slot(\s+name=["']?default["']?)?\s*\/?>.*?<\/slot>|<slot(\s+name=["']?default["']?)?\s*\/?>/i
} else {
regex = new RegExp(
`<slot\\s+name=["']?${name}["']?\\s*\\/?>.*?<\\/slot>|<slot\\s+name=["']?${name}["']?\\s*\\/?>`,
'i'
)
}
out = out.replace(regex, typeof el === 'string' ? el : HTMLtoString(el))
}
return { output: out, needHydration: hasEvents(res) }
}
/**
* Render the component to an HTMLElement for the browser.
*
* @throws {Error} if run outside the browser
*/
public async renderHTML(): Promise<HTMLElement> {
assert(getContext() === 'browser', 'renderHTML can only be run inside the browser')
const rendered = await this.renderOrCache()
const root = HTMLToElement(rendered)
for (const [name, el] of Object.entries(this.childs)) {
let slot: HTMLSlotElement | undefined | null
if (name === 'default') {
slot = root.querySelector('slot')
} else {
slot = root.querySelector<HTMLSlotElement>(`slot[name="${name}"]`)
}
if (slot) {
slot.replaceWith(typeof el === 'string' ? el : HTMLToElement(el))
}
}
return root
}
/**
* Tell the component to render itself.
*
* *works only in browser*
*/
public async triggerRender() {
// ignore automatics renders inside if we are not in the server
if (getContext() !== 'browser' /* || !this.shadowRoot*/) {
return
}
const rendered = await this.renderHTML()
this.innerHTML = ''
this.appendChild(rendered)
if (!this.mounted) {
this.mounted = true
this.didMount()
} else {
this.didUpdate()
}
}
private async renderOrCache(): Promise<SimpleHTMLElement> {
if (this.needRender || !this.lastRender) {
this.lastRender = await this.render()
}
return this.lastRender
}
}