From 86b37af7166b552605f39880ce371b466e92e929 Mon Sep 17 00:00:00 2001 From: Avior Date: Mon, 30 Jun 2025 22:41:18 +0200 Subject: [PATCH] help Signed-off-by: Avior --- .gitignore | 2 + Button.processed.ts | 60 ++++++ bun.lock | 218 +++++++++++++++++++++ package.json | 10 + src/compiler/codeshift.ts | 161 +++++++++++++++ src/compiler/index.ts | 220 +++++++++++++++++++++ src/components/badge/Badge.astro | 5 + src/components/badge/index.ts | 22 +++ src/components/button/Button.astro | 5 + src/components/button/index.ts | 74 +++++++ src/components/index.ts | 7 + src/components/list.ts | 11 ++ src/components/utils/AstroSSR.astro | 32 +++ src/components/utils/decorators.ts | 121 ++++++++++++ src/components/utils/utils.ts | 291 ++++++++++++++++++++++++++++ src/components/utils/web-element.ts | 287 +++++++++++++++++++++++++++ 16 files changed, 1526 insertions(+) create mode 100644 .gitignore create mode 100644 Button.processed.ts create mode 100644 bun.lock create mode 100644 package.json create mode 100644 src/compiler/codeshift.ts create mode 100644 src/compiler/index.ts create mode 100644 src/components/badge/Badge.astro create mode 100644 src/components/badge/index.ts create mode 100644 src/components/button/Button.astro create mode 100644 src/components/button/index.ts create mode 100644 src/components/index.ts create mode 100644 src/components/list.ts create mode 100644 src/components/utils/AstroSSR.astro create mode 100644 src/components/utils/decorators.ts create mode 100644 src/components/utils/utils.ts create mode 100644 src/components/utils/web-element.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d5b002 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +**.transformed.ts diff --git a/Button.processed.ts b/Button.processed.ts new file mode 100644 index 0000000..ccbf9ff --- /dev/null +++ b/Button.processed.ts @@ -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} + + ${this.iconRight} + + `; + } + private onClick = () => { + if (this.disabled) { + return; + } + console.log('Button clicked'); + }; +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..c2a7852 --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3a9d469 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/compiler/codeshift.ts b/src/compiler/codeshift.ts new file mode 100644 index 0000000..d03dd14 --- /dev/null +++ b/src/compiler/codeshift.ts @@ -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 + item: ObjectExpression +} + +interface EndField { + type: 'Literal' + item: Literal +} + +interface ArrayField { + type: 'Array' + items: Array + 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' diff --git a/src/compiler/index.ts b/src/compiler/index.ts new file mode 100644 index 0000000..fefd9fd --- /dev/null +++ b/src/compiler/index.ts @@ -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 + childs?: Array +} + +// 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): 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): Record { + const attrs: Record = {} + 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 + 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 = 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}`) diff --git a/src/components/badge/Badge.astro b/src/components/badge/Badge.astro new file mode 100644 index 0000000..439795e --- /dev/null +++ b/src/components/badge/Badge.astro @@ -0,0 +1,5 @@ +--- +import Item from '.' +import AstroSSR from '../utils/AstroSSR.astro' +--- + diff --git a/src/components/badge/index.ts b/src/components/badge/index.ts new file mode 100644 index 0000000..189039b --- /dev/null +++ b/src/components/badge/index.ts @@ -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 { + return html` +
+
+ ${this.badge} + +
+
+ ` + } +} diff --git a/src/components/button/Button.astro b/src/components/button/Button.astro new file mode 100644 index 0000000..46c3621 --- /dev/null +++ b/src/components/button/Button.astro @@ -0,0 +1,5 @@ +--- +import Btn from '.' +import AstroSSR from '../utils/AstroSSR.astro' +--- + diff --git a/src/components/button/index.ts b/src/components/button/index.ts new file mode 100644 index 0000000..c0e98fd --- /dev/null +++ b/src/components/button/index.ts @@ -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} + + ${this.iconRight} + + ` + } + + private onClick = () => { + if (this.disabled) { return } + console.log('Button clicked') + } +} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..952261b --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,7 @@ +import WebElement from './utils/web-element' + +export { + WebElement +} +export * from './utils/decorators' +export * from './utils/utils' diff --git a/src/components/list.ts b/src/components/list.ts new file mode 100644 index 0000000..b647b69 --- /dev/null +++ b/src/components/list.ts @@ -0,0 +1,11 @@ +import Button from './button' +import Badge from './badge' + +const components = [ + Button, + Badge +] + +export function loadComponents() { + return components +} diff --git a/src/components/utils/AstroSSR.astro b/src/components/utils/AstroSSR.astro new file mode 100644 index 0000000..a7a47e3 --- /dev/null +++ b/src/components/utils/AstroSSR.astro @@ -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 + slots?: typeof Astro.slots +} + +const omp = new Astro.props.component() +const Tag = omp.getConfig().tag + +// parse Astro slots +const slots : Record = {} +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() +--- + + 0 ? JSON.stringify(slots) : undefined} /> diff --git a/src/components/utils/decorators.ts b/src/components/utils/decorators.ts new file mode 100644 index 0000000..a28e0d7 --- /dev/null +++ b/src/components/utils/decorators.ts @@ -0,0 +1,121 @@ +import { WebElement } from '..' + +interface ComponentConfig { + tag: `${string}-${string}` + attrs?: Record + 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) { + 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 ?? {} + } +} diff --git a/src/components/utils/utils.ts b/src/components/utils/utils.ts new file mode 100644 index 0000000..75da578 --- /dev/null +++ b/src/components/utils/utils.ts @@ -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}` +} + +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 + events?: Record + childs?: Array +} + +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): { attrs: Record, events: Record } { + const attrs: Record = {} + const events: Record = {} + 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 = []): 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 = [] + + 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(``, i)) { + depth-- + if (depth === 0) { + i += (``).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): SimpleHTMLElement +export function html(strings: TemplateStringsArray | string, ...values: Array): 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) + .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> + +/** + * 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): 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 { + 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 { + 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') + } +} diff --git a/src/components/utils/web-element.ts b/src/components/utils/web-element.ts new file mode 100644 index 0000000..13a256d --- /dev/null +++ b/src/components/utils/web-element.ts @@ -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 + } = { + tag: 'web-element' + } + + /** + * childs of the current element that will be rendered inside `` elements + */ + public childs: Record = {} + + /** + * 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 { + const conf = this.__config + const self = new this() + .setProps(props) + return html`<${conf.tag}>${(await self.renderString()).output}` + } + + 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) + 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 { + return html`
` + } + + /** + * 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): this + public setSlots(slots: Record | 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 { + const props: Record = {} + for (const key of Object.keys(this.getConfig().attrs ?? {})) { + props[key] = this[key] + } + return props + } + + public setProps(props?: Partial>) { + 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 tags + let out = HTMLtoString(res) + + for (const [name, el] of Object.entries(this.childs)) { + let regex: RegExp + if (name === 'default') { + regex = /.*?<\/slot>|/i + } else { + regex = new RegExp( + `.*?<\\/slot>|`, + '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 { + 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(`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 { + if (this.needRender || !this.lastRender) { + this.lastRender = await this.render() + } + return this.lastRender + } +}