2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
**.transformed.ts
|
60
Button.processed.ts
Normal file
60
Button.processed.ts
Normal 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
218
bun.lock
Normal 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
10
package.json
Normal 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
161
src/compiler/codeshift.ts
Normal 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
220
src/compiler/index.ts
Normal 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}`)
|
5
src/components/badge/Badge.astro
Normal file
5
src/components/badge/Badge.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import Item from '.'
|
||||
import AstroSSR from '../utils/AstroSSR.astro'
|
||||
---
|
||||
<AstroSSR component={Item} props={Astro.props} slots={Astro.slots} />
|
22
src/components/badge/index.ts
Normal file
22
src/components/badge/index.ts
Normal 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>
|
||||
`
|
||||
}
|
||||
}
|
5
src/components/button/Button.astro
Normal file
5
src/components/button/Button.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import Btn from '.'
|
||||
import AstroSSR from '../utils/AstroSSR.astro'
|
||||
---
|
||||
<AstroSSR component={Btn} props={Astro.props} slots={Astro.slots} />
|
74
src/components/button/index.ts
Normal file
74
src/components/button/index.ts
Normal 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
7
src/components/index.ts
Normal 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
11
src/components/list.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import Button from './button'
|
||||
import Badge from './badge'
|
||||
|
||||
const components = [
|
||||
Button,
|
||||
Badge
|
||||
]
|
||||
|
||||
export function loadComponents() {
|
||||
return components
|
||||
}
|
32
src/components/utils/AstroSSR.astro
Normal file
32
src/components/utils/AstroSSR.astro
Normal 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} />
|
121
src/components/utils/decorators.ts
Normal file
121
src/components/utils/decorators.ts
Normal 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 ?? {}
|
||||
}
|
||||
}
|
291
src/components/utils/utils.ts
Normal file
291
src/components/utils/utils.ts
Normal 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('"', '"')}"`).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('"', '"')) 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('"', '"')}"`)
|
||||
.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('"', '"'), 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')
|
||||
}
|
||||
}
|
287
src/components/utils/web-element.ts
Normal file
287
src/components/utils/web-element.ts
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user