diff --git a/bun.lock b/bun.lock index 2781ca1..0eb2299 100644 --- a/bun.lock +++ b/bun.lock @@ -5,21 +5,30 @@ "": { "name": "libit", "dependencies": { + "commander": "^14.0.2", "esbuild": "^0.19.0", + "expect": "^30.2.0", + "glob": "^13.0.0", }, "devDependencies": { "@types/bun": "latest", "typescript": "^5.0.0", }, "peerDependencies": { + "playwright": "^1.40.0", "typescript": "^5.0.0", }, "optionalPeers": [ + "playwright", "typescript", ], }, }, "packages": { + "@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/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -66,20 +75,122 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + + "@jest/diff-sequences": ["@jest/diff-sequences@30.0.1", "", {}, "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw=="], + + "@jest/expect-utils": ["@jest/expect-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0" } }, "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA=="], + + "@jest/get-type": ["@jest/get-type@30.1.0", "", {}, "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA=="], + + "@jest/pattern": ["@jest/pattern@30.0.1", "", { "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" } }, "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA=="], + + "@jest/schemas": ["@jest/schemas@30.0.5", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA=="], + + "@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.45", "", {}, "sha512-qJcFVfCa5jxBFSuv7S5WYbA8XdeCPmhnaVVfX/2Y6L8WYg8sk3XY2+6W0zH+3mq1Cz+YC7Ki66HfqX6IHAwnkg=="], + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + + "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "expect": ["expect@30.2.0", "", { "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "jest-diff": ["jest-diff@30.2.0", "", { "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "pretty-format": "30.2.0" } }, "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A=="], + + "jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], + + "jest-message-util": ["jest-message-util@30.2.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw=="], + + "jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], + + "jest-regex-util": ["jest-regex-util@30.0.1", "", {}, "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA=="], + + "jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..43f64f8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,986 @@ +{ + "name": "@b9g/libuild", + "version": "0.1.20", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@b9g/libuild", + "version": "0.1.20", + "license": "MIT", + "dependencies": { + "esbuild": "^0.19.0", + "expect": "^30.2.0" + }, + "bin": { + "libuild": "dist/src/cli.js" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.0.0" + }, + "engines": { + "bun": ">=1.0.0", + "node": ">=18.20.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "license": "MIT" + }, + "node_modules/@types/bun": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.5.tgz", + "integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.5" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bun-types": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.5.tgz", + "integrity": "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.19.12", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index bde5433..2f7f409 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,21 @@ "test": "bun test" }, "dependencies": { - "esbuild": "^0.19.0" + "commander": "^14.0.2", + "esbuild": "^0.19.0", + "expect": "^30.2.0", + "glob": "^13.0.0" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "playwright": "^1.40.0" }, "peerDependenciesMeta": { "typescript": { "optional": true + }, + "playwright": { + "optional": true } }, "devDependencies": { @@ -53,30 +60,57 @@ "exports": { ".": { "types": "./dist/src/libuild.d.ts", - "import": "./dist/src/libuild.js", - "require": "./dist/src/libuild.cjs" + "import": "./dist/src/libuild.js" }, "./cli": { "types": "./dist/src/cli.d.ts", - "import": "./dist/src/cli.js", - "require": "./dist/src/cli.cjs" + "import": "./dist/src/cli.js" }, "./cli.js": { "types": "./dist/src/cli.d.ts", - "import": "./dist/src/cli.js", - "require": "./dist/src/cli.cjs" + "import": "./dist/src/cli.js" }, "./libuild": { "types": "./dist/src/libuild.d.ts", - "import": "./dist/src/libuild.js", - "require": "./dist/src/libuild.cjs" + "import": "./dist/src/libuild.js" }, "./libuild.js": { "types": "./dist/src/libuild.d.ts", - "import": "./dist/src/libuild.js", - "require": "./dist/src/libuild.cjs" + "import": "./dist/src/libuild.js" + }, + "./package.json": "./dist/package.json", + "./test": { + "types": "./dist/src/test.d.ts", + "import": "./dist/src/test.js" + }, + "./test.js": { + "types": "./dist/src/test.d.ts", + "import": "./dist/src/test.js" + }, + "./test-bun": { + "types": "./dist/src/test-bun.d.ts", + "import": "./dist/src/test-bun.js" }, - "./package.json": "./dist/package.json" + "./test-bun.js": { + "types": "./dist/src/test-bun.d.ts", + "import": "./dist/src/test-bun.js" + }, + "./test-node": { + "types": "./dist/src/test-node.d.ts", + "import": "./dist/src/test-node.js" + }, + "./test-node.js": { + "types": "./dist/src/test-node.d.ts", + "import": "./dist/src/test-node.js" + }, + "./test-browser": { + "types": "./dist/src/test-browser.d.ts", + "import": "./dist/src/test-browser.js" + }, + "./test-browser.js": { + "types": "./dist/src/test-browser.d.ts", + "import": "./dist/src/test-browser.js" + } }, "files": [ "dist/", diff --git a/src/cli.ts b/src/cli.ts index 08657be..fab4ec0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,204 +1,91 @@ #!/usr/bin/env node -import {parseArgs} from "util"; +import { Command } from "commander"; import * as Path from "path"; -import {build, publish} from "./libuild.ts"; - -const {values, positionals} = parseArgs({ - args: process.argv.slice(2), - options: { - help: {type: "boolean", short: "h"}, - version: {type: "boolean", short: "v"}, - save: {type: "boolean"}, - "no-save": {type: "boolean"}, - }, - allowPositionals: true, - strict: false, // Allow unknown options to be passed through -}); - -const HELP_TEXT = ` -libuild - Zero-config library builds - -Usage: - libuild [command] [directory] [options] - -Commands: - build Build the library (default command) - publish Build and publish the library - -Arguments: - directory Optional directory to build (defaults to current directory) - -Options: - --save Update root package.json to point to dist files - --no-save Skip package.json updates (for publish command) - -IMPORTANT: - • libuild is zero-config - there is NO libuild.config.js file - • Configuration comes from your package.json (main, module, exports, etc.) - • Use --save to regenerate package.json fields based on built files - • Invalid bin/exports paths are automatically cleaned up with --save - -For publish command, all additional flags are forwarded to npm publish. - -Examples: - libuild # Build the library in current directory - libuild build # Same as above - libuild ../other-proj # Build library in ../other-proj - libuild build --save # Build and update package.json for npm link - libuild publish ../lib # Build and publish library in ../lib - libuild publish --dry-run --tag beta # Build and publish with npm flags -`; - -async function main() { - if (values.help) { - console.log(HELP_TEXT); - process.exit(0); - } - - if (values.version) { - const pkg = await import("../package.json"); - console.log(pkg.version); - process.exit(0); - } - - // Parse command and optional directory more carefully - // Need to handle that npm flag values might be in positionals due to strict: false - const command = positionals[0] || "build"; - - // Find directory by looking for positionals that are actual directory paths - // Skip flag values by checking if previous positional was a flag that expects a value - let targetDir: string | undefined; - for (let i = 1; i < positionals.length; i++) { - const arg = positionals[i]; - - // Skip if this looks like it's a value for a flag - const flagValueFlags = ["--tag", "--access", "--registry", "--otp", "--workspace"]; - const isValueForFlag = flagValueFlags.some(flag => { - const flagIndex = process.argv.indexOf(flag); - const argIndex = process.argv.indexOf(arg); - return flagIndex !== -1 && argIndex === flagIndex + 1; - }); - - if (isValueForFlag) { - continue; - } - - // Check if this looks like a valid directory path - if (!arg.startsWith("--")) { - try { - // Only treat as directory if it looks like a project directory - // Skip system paths like /bin/sh, /usr/bin, etc. - const isSystemPath = Path.isAbsolute(arg) && ( - arg.startsWith("/bin/") || - arg.startsWith("/usr/") || - arg.startsWith("/etc/") || - arg.startsWith("/var/") || - arg.startsWith("/opt/") || - arg.startsWith("/tmp/") || - arg.startsWith("/System/") || - arg.startsWith("/Applications/") - ); - - if (isSystemPath) { - continue; - } - - const resolvedPath = Path.resolve(arg); - - // Prefer relative paths or current directory references - if (arg.includes("/") || arg.includes("\\") || arg === "." || arg === ".." || - arg.startsWith("./") || arg.startsWith("../")) { - targetDir = arg; - break; - } - - // For simple names, only use if they exist as directories in current working directory - const fs = await import("fs/promises"); - try { - const stat = await fs.stat(resolvedPath); - if (stat.isDirectory() && !Path.isAbsolute(arg)) { - targetDir = arg; - break; - } - } catch { - // Doesn't exist, skip it - } - } catch { - // Invalid path, skip it - } +import { build, publish } from "./libuild.ts"; +import { runTests, type Platform } from "./test-runner.ts"; + +const program = new Command(); + +program + .name("libuild") + .description("Zero-config library builds") + .version("0.1.22"); + +program + .command("build", { isDefault: true }) + .description("Build the library") + .argument("[directory]", "Directory to build", ".") + .option("--save", "Update root package.json to point to dist files") + .action(async (directory: string, options: { save?: boolean }) => { + const cwd = Path.resolve(directory); + await build(cwd, options.save || false); + }); + +program + .command("publish") + .description("Build and publish the library") + .argument("[directory]", "Directory to build and publish", ".") + .option("--no-save", "Skip package.json updates") + .option("--dry-run", "Perform a dry run") + .option("--tag ", "Publish with a specific tag") + .option("--access ", "Set access level (public/restricted)") + .option("--registry ", "Use a specific registry") + .option("--otp ", "One-time password for 2FA") + .option("--provenance", "Generate provenance statement") + .action(async (directory: string, options: Record) => { + const cwd = Path.resolve(directory); + const shouldSave = options.save !== false; + + // Build npm publish args from options + const npmArgs: string[] = []; + if (options.dryRun) npmArgs.push("--dry-run"); + if (options.tag) npmArgs.push("--tag", options.tag); + if (options.access) npmArgs.push("--access", options.access); + if (options.registry) npmArgs.push("--registry", options.registry); + if (options.otp) npmArgs.push("--otp", options.otp); + if (options.provenance) npmArgs.push("--provenance"); + + await publish(cwd, shouldSave, npmArgs); + }); + +program + .command("test") + .description("Run tests across platforms") + .argument("[directory]", "Directory containing tests", ".") + .option("-p, --platform ", "Platforms to test on (bun, node, chromium, firefox, webkit)") + .option("--debug", "Keep browser open for debugging") + .option("-w, --watch", "Watch mode - re-run tests on file changes") + .option("--timeout ", "Test timeout in milliseconds", "60000") + .action(async (directory: string, options: { + platform?: string[]; + debug?: boolean; + watch?: boolean; + timeout?: string; + }) => { + const cwd = Path.resolve(directory); + + // Validate platforms + const validPlatforms: Platform[] = ["bun", "node", "chromium", "firefox", "webkit"]; + const platforms: Platform[] = options.platform?.length + ? options.platform.filter((p): p is Platform => validPlatforms.includes(p as Platform)) + : ["bun"]; + + if (options.platform?.length && platforms.length !== options.platform.length) { + const invalid = options.platform.filter(p => !validPlatforms.includes(p as Platform)); + console.error(`Invalid platform(s): ${invalid.join(", ")}`); + console.error(`Valid platforms: ${validPlatforms.join(", ")}`); + process.exit(1); } - } - - const cwd = targetDir ? Path.resolve(targetDir) : process.cwd(); - // Determine save behavior - const shouldSave = values.save || (command === "publish" && !values["no-save"]); - - // Extract and validate additional arguments for forwarding to npm publish - const allowedNpmFlags = [ - "--dry-run", "--tag", "--access", "--registry", "--otp", "--provenance", - "--workspace", "--workspaces", "--include-workspace-root" - ]; - - const rawExtraArgs = process.argv.slice(2).filter(arg => - !["build", "publish"].includes(arg) && - !["--save", "--no-save", "--help", "-h", "--version", "-v"].includes(arg) && - arg !== targetDir // Don't filter out the target directory - ); - - // Validate npm arguments for security - const extraArgs: string[] = []; - for (let i = 0; i < rawExtraArgs.length; i++) { - const arg = rawExtraArgs[i]; - - // Check if it's a known flag - if (arg.startsWith("--")) { - const flagName = arg.split("=")[0]; // Handle --flag=value format - if (allowedNpmFlags.includes(flagName)) { - extraArgs.push(arg); - // If this flag expects a value and it's not in --flag=value format, include the next argument - if (!arg.includes("=") && ["--tag", "--access", "--registry", "--otp", "--workspace"].includes(flagName)) { - if (i + 1 < rawExtraArgs.length && !rawExtraArgs[i + 1].startsWith("--")) { - extraArgs.push(rawExtraArgs[i + 1]); - i++; // Skip the next argument as it's the value - } - } - } else { - console.warn(`Warning: Ignoring unknown/unsafe npm flag: ${arg}`); - } - } else { - // Non-flag arguments are only allowed as values to flags we've already validated - // Check if this is a value for a flag that expects one - const prevArg = i > 0 ? rawExtraArgs[i - 1] : ""; - const isPrevArgValueFlag = ["--tag", "--access", "--registry", "--otp", "--workspace"].includes(prevArg) && - !prevArg.includes("="); - - if (isPrevArgValueFlag && extraArgs.includes(prevArg)) { - // This argument is the value for a flag that was already accepted - extraArgs.push(arg); - } else { - console.warn(`Warning: Ignoring unexpected argument: ${arg}`); - } - } - } + const success = await runTests({ + cwd, + platforms, + debug: options.debug || false, + watch: options.watch || false, + timeout: parseInt(options.timeout || "60000", 10), + }); - try { - switch (command) { - case "build": - await build(cwd, shouldSave); - break; - case "publish": - await publish(cwd, shouldSave, extraArgs); - break; - default: - console.error(`Unknown command: ${command}`); - console.log(HELP_TEXT); - process.exit(1); - } - } catch (error: any) { - console.error("Error:", error.message); - process.exit(1); - } -} + process.exit(success ? 0 : 1); + }); -main(); +program.parse(); diff --git a/src/libuild.ts b/src/libuild.ts index 3c79de1..367c46c 100644 --- a/src/libuild.ts +++ b/src/libuild.ts @@ -1093,7 +1093,8 @@ export async function build(cwd: string, save: boolean = false): Promise<{distPk // External only JSON files (npm deps handled by packages: "external", Node.js built-ins by platform: "node") const externalDeps = [ "*.json", // Let Node.js handle JSON imports natively - "esbuild" // Explicit external to suppress require.resolve warning from esbuild's own code + "esbuild", // Explicit external to suppress require.resolve warning from esbuild's own code + "bun:*", // Bun built-ins (bun:test, bun:sqlite, etc.) ]; // Prepare entry points for batch building diff --git a/src/test-browser.ts b/src/test-browser.ts new file mode 100644 index 0000000..86e70e2 --- /dev/null +++ b/src/test-browser.ts @@ -0,0 +1,409 @@ +/** + * Browser test shim for @b9g/libuild/test + * + * Implements a minimal test runner that works in browsers. + * Results are exposed via globalThis.__LIBUILD_TEST__ for Playwright to poll. + */ + +/** + * Minimal browser-compatible expect implementation + */ +class ExpectError extends Error { + constructor(message: string) { + super(message); + this.name = "ExpectError"; + } +} + +function stringify(value: unknown): string { + if (value === undefined) return "undefined"; + if (value === null) return "null"; + if (typeof value === "function") return "[Function]"; + if (typeof value === "symbol") return value.toString(); + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function deepEqual(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (typeof a !== typeof b) return false; + if (a === null || b === null) return a === b; + if (typeof a !== "object") return a === b; + + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + return a.every((item, i) => deepEqual(item, b[i])); + } + + if (Array.isArray(a) !== Array.isArray(b)) return false; + + const aKeys = Object.keys(a as object); + const bKeys = Object.keys(b as object); + if (aKeys.length !== bKeys.length) return false; + + return aKeys.every((key) => + deepEqual( + (a as Record)[key], + (b as Record)[key] + ) + ); +} + +interface Matchers { + toBe(expected: unknown): void; + toEqual(expected: unknown): void; + toStrictEqual(expected: unknown): void; + toBeTruthy(): void; + toBeFalsy(): void; + toBeNull(): void; + toBeUndefined(): void; + toBeDefined(): void; + toBeNaN(): void; + toBeGreaterThan(expected: number): void; + toBeGreaterThanOrEqual(expected: number): void; + toBeLessThan(expected: number): void; + toBeLessThanOrEqual(expected: number): void; + toContain(expected: unknown): void; + toHaveLength(expected: number): void; + toMatch(expected: RegExp | string): void; + toThrow(expected?: string | RegExp | Error): void; + toBeInstanceOf(expected: new (...args: any[]) => any): void; + not: Matchers; +} + +function createMatchers(actual: unknown, negated = false): Matchers { + const assert = (pass: boolean, message: string, negatedMessage: string) => { + const shouldPass = negated ? !pass : pass; + if (!shouldPass) { + throw new ExpectError(negated ? negatedMessage : message); + } + }; + + const matchers: Matchers = { + toBe(expected: unknown) { + assert( + actual === expected, + `Expected ${stringify(actual)} to be ${stringify(expected)}`, + `Expected ${stringify(actual)} not to be ${stringify(expected)}` + ); + }, + toEqual(expected: unknown) { + assert( + deepEqual(actual, expected), + `Expected ${stringify(actual)} to equal ${stringify(expected)}`, + `Expected ${stringify(actual)} not to equal ${stringify(expected)}` + ); + }, + toStrictEqual(expected: unknown) { + assert( + deepEqual(actual, expected), + `Expected ${stringify(actual)} to strictly equal ${stringify(expected)}`, + `Expected ${stringify(actual)} not to strictly equal ${stringify(expected)}` + ); + }, + toBeTruthy() { + assert( + Boolean(actual), + `Expected ${stringify(actual)} to be truthy`, + `Expected ${stringify(actual)} not to be truthy` + ); + }, + toBeFalsy() { + assert( + !actual, + `Expected ${stringify(actual)} to be falsy`, + `Expected ${stringify(actual)} not to be falsy` + ); + }, + toBeNull() { + assert( + actual === null, + `Expected ${stringify(actual)} to be null`, + `Expected ${stringify(actual)} not to be null` + ); + }, + toBeUndefined() { + assert( + actual === undefined, + `Expected ${stringify(actual)} to be undefined`, + `Expected ${stringify(actual)} not to be undefined` + ); + }, + toBeDefined() { + assert( + actual !== undefined, + `Expected ${stringify(actual)} to be defined`, + `Expected ${stringify(actual)} not to be defined` + ); + }, + toBeNaN() { + assert( + Number.isNaN(actual), + `Expected ${stringify(actual)} to be NaN`, + `Expected ${stringify(actual)} not to be NaN` + ); + }, + toBeGreaterThan(expected: number) { + assert( + (actual as number) > expected, + `Expected ${stringify(actual)} to be greater than ${expected}`, + `Expected ${stringify(actual)} not to be greater than ${expected}` + ); + }, + toBeGreaterThanOrEqual(expected: number) { + assert( + (actual as number) >= expected, + `Expected ${stringify(actual)} to be greater than or equal to ${expected}`, + `Expected ${stringify(actual)} not to be greater than or equal to ${expected}` + ); + }, + toBeLessThan(expected: number) { + assert( + (actual as number) < expected, + `Expected ${stringify(actual)} to be less than ${expected}`, + `Expected ${stringify(actual)} not to be less than ${expected}` + ); + }, + toBeLessThanOrEqual(expected: number) { + assert( + (actual as number) <= expected, + `Expected ${stringify(actual)} to be less than or equal to ${expected}`, + `Expected ${stringify(actual)} not to be less than or equal to ${expected}` + ); + }, + toContain(expected: unknown) { + const contains = Array.isArray(actual) + ? actual.includes(expected) + : typeof actual === "string" && typeof expected === "string" + ? actual.includes(expected) + : false; + assert( + contains, + `Expected ${stringify(actual)} to contain ${stringify(expected)}`, + `Expected ${stringify(actual)} not to contain ${stringify(expected)}` + ); + }, + toHaveLength(expected: number) { + const len = (actual as { length: number }).length; + assert( + len === expected, + `Expected length ${len} to be ${expected}`, + `Expected length ${len} not to be ${expected}` + ); + }, + toMatch(expected: RegExp | string) { + const regex = typeof expected === "string" ? new RegExp(expected) : expected; + assert( + regex.test(actual as string), + `Expected ${stringify(actual)} to match ${expected}`, + `Expected ${stringify(actual)} not to match ${expected}` + ); + }, + toThrow(expected?: string | RegExp | Error) { + let threw = false; + let error: unknown; + try { + (actual as () => void)(); + } catch (e) { + threw = true; + error = e; + } + if (expected === undefined) { + assert( + threw, + `Expected function to throw`, + `Expected function not to throw` + ); + } else if (typeof expected === "string") { + assert( + threw && (error as Error).message.includes(expected), + `Expected function to throw error containing "${expected}"`, + `Expected function not to throw error containing "${expected}"` + ); + } else if (expected instanceof RegExp) { + assert( + threw && expected.test((error as Error).message), + `Expected function to throw error matching ${expected}`, + `Expected function not to throw error matching ${expected}` + ); + } else if (expected instanceof Error) { + assert( + threw && (error as Error).message === expected.message, + `Expected function to throw ${expected.message}`, + `Expected function not to throw ${expected.message}` + ); + } + }, + toBeInstanceOf(expected: new (...args: any[]) => any) { + assert( + actual instanceof expected, + `Expected ${stringify(actual)} to be instance of ${expected.name}`, + `Expected ${stringify(actual)} not to be instance of ${expected.name}` + ); + }, + get not() { + return createMatchers(actual, !negated); + }, + }; + + return matchers; +} + +export function expect(actual: unknown): Matchers { + return createMatchers(actual); +} + +// Global state for playwright to poll +declare global { + var __LIBUILD_TEST__: { + ended: boolean; + failed: number; + passed: number; + errors: Array<{ name: string; error: string }>; + }; +} + +globalThis.__LIBUILD_TEST__ = { + ended: false, + failed: 0, + passed: 0, + errors: [], +}; + +type TestFn = () => void | Promise; +type HookFn = () => void | Promise; + +interface Test { + name: string; + fn: TestFn; + suite: Suite; +} + +interface Suite { + name: string; + parent: Suite | null; + beforeAll: HookFn[]; + afterAll: HookFn[]; + beforeEach: HookFn[]; + afterEach: HookFn[]; +} + +const tests: Test[] = []; +const rootSuite: Suite = { + name: "", + parent: null, + beforeAll: [], + afterAll: [], + beforeEach: [], + afterEach: [], +}; +let currentSuite = rootSuite; + +export function describe(name: string, fn: () => void) { + const suite: Suite = { + name, + parent: currentSuite, + beforeAll: [], + afterAll: [], + beforeEach: [], + afterEach: [], + }; + const prev = currentSuite; + currentSuite = suite; + fn(); + currentSuite = prev; +} + +export function test(name: string, fn: TestFn) { + tests.push({ name, fn, suite: currentSuite }); +} + +export const it = test; + +export function beforeEach(fn: HookFn) { + currentSuite.beforeEach.push(fn); +} + +export function afterEach(fn: HookFn) { + currentSuite.afterEach.push(fn); +} + +export function beforeAll(fn: HookFn) { + currentSuite.beforeAll.push(fn); +} + +export function afterAll(fn: HookFn) { + currentSuite.afterAll.push(fn); +} + +// Collect suite chain for a test +function getSuiteChain(suite: Suite): Suite[] { + const chain: Suite[] = []; + let s: Suite | null = suite; + while (s) { + chain.unshift(s); + s = s.parent; + } + return chain; +} + +// Get full test name +function getFullName(t: Test): string { + const chain = getSuiteChain(t.suite); + const names = chain.map((s) => s.name).filter(Boolean); + names.push(t.name); + return names.join(" > "); +} + +// Auto-run after all modules loaded +queueMicrotask(async () => { + const suiteRan = new Set(); + + for (const t of tests) { + const fullName = getFullName(t); + const chain = getSuiteChain(t.suite); + + try { + // Run beforeAll for suites that haven't run yet + for (const suite of chain) { + if (!suiteRan.has(suite)) { + for (const hook of suite.beforeAll) await hook(); + suiteRan.add(suite); + } + } + + // Run beforeEach hooks (outer to inner) + for (const suite of chain) { + for (const hook of suite.beforeEach) await hook(); + } + + await t.fn(); + + // Run afterEach hooks (inner to outer) + for (const suite of [...chain].reverse()) { + for (const hook of suite.afterEach) await hook(); + } + + console.log("✓", fullName); + globalThis.__LIBUILD_TEST__.passed++; + } catch (e: any) { + console.error("✗", fullName); + console.error(" ", e.message); + globalThis.__LIBUILD_TEST__.failed++; + globalThis.__LIBUILD_TEST__.errors.push({ + name: fullName, + error: e.message || String(e), + }); + } + } + + // Run afterAll hooks for all suites (inner to outer) + for (const suite of [...suiteRan].reverse()) { + for (const hook of suite.afterAll) await hook(); + } + + globalThis.__LIBUILD_TEST__.ended = true; +}); diff --git a/src/test-bun.ts b/src/test-bun.ts new file mode 100644 index 0000000..252d3e7 --- /dev/null +++ b/src/test-bun.ts @@ -0,0 +1,16 @@ +/** + * Bun test shim for @b9g/libuild/test + * + * Simply re-exports bun:test which already has Jest-compatible APIs. + */ + +export { + describe, + test, + it, + expect, + beforeAll, + afterAll, + beforeEach, + afterEach, +} from "bun:test"; diff --git a/src/test-node.ts b/src/test-node.ts new file mode 100644 index 0000000..39604f1 --- /dev/null +++ b/src/test-node.ts @@ -0,0 +1,29 @@ +/** + * Node.js test shim for @b9g/libuild/test + * + * Combines node:test runner with Jest's expect matchers. + */ + +import { + describe, + test, + it, + before, + after, + beforeEach, + afterEach, +} from "node:test"; + +// Re-export expect from jest's standalone package +export { expect } from "expect"; + +// Re-export node:test primitives +export { + describe, + test, + it, + before as beforeAll, + after as afterAll, + beforeEach, + afterEach, +}; diff --git a/src/test-runner.ts b/src/test-runner.ts new file mode 100644 index 0000000..8e75852 --- /dev/null +++ b/src/test-runner.ts @@ -0,0 +1,578 @@ +/** + * @b9g/libuild test runner + * + * Cross-platform test execution for Bun, Node, and browsers. + */ + +import * as FS from "fs/promises"; +import * as Path from "path"; +import { createServer, type Server } from "http"; +import * as ESBuild from "esbuild"; +import { fileURLToPath } from "url"; +import { createRequire } from "module"; + +const __dirname = Path.dirname(fileURLToPath(import.meta.url)); + +export type Platform = "bun" | "node" | "chromium" | "firefox" | "webkit"; + +export interface TestRunnerOptions { + /** Working directory */ + cwd: string; + /** Test file patterns */ + patterns: string[]; + /** Platforms to run on (bun, node, chromium, firefox, webkit) */ + platforms: Platform[]; + /** Enable debug mode (keeps browser open) */ + debug: boolean; + /** Test timeout in ms */ + timeout: number; + /** Watch mode */ + watch: boolean; +} + +export interface TestResult { + platform: string; + passed: number; + failed: number; + errors: Array<{ name: string; error: string }>; +} + +const DEFAULT_PATTERNS = [ + "**/*.test.ts", + "**/*.test.tsx", + "**/*.test.js", + "**/*.test.jsx", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/test/**/*.ts", + "**/test/**/*.tsx", + "**/test/**/*.js", + "**/test/**/*.jsx", +]; + +const IGNORE_PATTERNS = [ + "**/node_modules/**", + "**/dist/**", + "**/.git/**", + "**/coverage/**", +]; + +/** + * Find test files matching patterns + */ +async function findTestFiles(cwd: string, patterns: string[]): Promise { + const { glob } = await import("glob"); + + const files: string[] = []; + for (const pattern of patterns) { + const matches = await glob(pattern, { + cwd, + ignore: IGNORE_PATTERNS, + absolute: true, + }); + files.push(...matches); + } + + // Deduplicate + return [...new Set(files)]; +} + +/** + * Generate entry point that imports all test files + */ +function generateTestEntry(testFiles: string[], platform: string): string { + const imports = testFiles + .map((file, i) => `import "${file.replace(/\\/g, "/")}";`) + .join("\n"); + + return `// Auto-generated test entry for ${platform} +${imports} +`; +} + +/** + * Check if platform is a browser + */ +function isBrowserPlatform(platform: Platform): platform is "chromium" | "firefox" | "webkit" { + return platform === "chromium" || platform === "firefox" || platform === "webkit"; +} + +/** + * Bundle tests for a specific platform + */ +async function bundleTests( + testFiles: string[], + platform: Platform, + outDir: string, + cwd: string +): Promise { + const entryContent = generateTestEntry(testFiles, platform); + const entryPath = Path.join(outDir, `entry-${platform}.ts`); + const outPath = Path.join(outDir, `bundle-${platform}.js`); + + await FS.writeFile(entryPath, entryContent); + + // Determine the shim path based on platform + const isBrowser = isBrowserPlatform(platform); + const shimName = isBrowser ? "test-browser" : `test-${platform}`; + + // For development, use source files; for installed package, use dist + let shimPath: string; + try { + // Try to resolve from the package (installed mode) + shimPath = require.resolve(`@b9g/libuild/${shimName}`); + } catch { + // Development mode - use relative path + shimPath = Path.join(__dirname, `${shimName}.js`); + } + + // For Node/Bun, we need to inject a require shim for CJS interop + const requireShim = ` +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +`; + + const buildOptions: ESBuild.BuildOptions = { + entryPoints: [entryPath], + bundle: true, + format: "esm", + outfile: outPath, + platform: isBrowser ? "browser" : "node", + target: isBrowser ? "es2020" : "node18", + // Replace @b9g/libuild/test with platform-specific shim + alias: { + "@b9g/libuild/test": shimPath, + }, + // External runtime-specific modules + external: platform === "bun" ? ["bun:test"] : [], + // Inject require shim for node/bun to handle CJS deps like expect/chalk + ...(isBrowser ? {} : { banner: { js: requireShim } }), + // Define for dead code elimination + define: { + "process.env.NODE_ENV": '"test"', + }, + logLevel: "warning", + }; + + await ESBuild.build(buildOptions); + + return outPath; +} + +/** + * Parse TAP output to extract test results + * Node TAP output uses "type: 'test'" for actual tests vs "type: 'suite'" for describe blocks + */ +function parseTapOutput(output: string): { passed: number; failed: number; errors: Array<{ name: string; error: string }> } { + let passed = 0; + let failed = 0; + const errors: Array<{ name: string; error: string }> = []; + + const lines = output.split("\n"); + let lastTestName = ""; + let lastTestPassed = true; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // TAP format: "ok 1 - test name" or "not ok 1 - test name" + const okMatch = line.match(/^\s*ok \d+ - (.+)/); + const notOkMatch = line.match(/^\s*not ok \d+ - (.+)/); + + if (okMatch) { + lastTestName = okMatch[1]; + lastTestPassed = true; + } else if (notOkMatch) { + lastTestName = notOkMatch[1]; + lastTestPassed = false; + } + + // Check if this is a test (not a suite) by looking for type: 'test' + if (line.includes("type: 'test'") || line.includes('type: "test"')) { + if (lastTestPassed) { + passed++; + } else { + failed++; + errors.push({ name: lastTestName, error: "Test failed" }); + } + } + } + + return { passed, failed, errors }; +} + +/** + * Run tests in Node.js using node:test + */ +async function runNodeTests(bundlePath: string, timeout: number): Promise { + const { spawn } = await import("child_process"); + + return new Promise((resolve) => { + const child = spawn("node", ["--test", "--test-reporter=tap", bundlePath], { + stdio: ["pipe", "pipe", "pipe"], + timeout, + }); + + let stdout = ""; + let stderr = ""; + // Track pending test results for streaming output + let pendingTest: { name: string; passed: boolean } | null = null; + const printedTests = new Set(); + + child.stdout?.on("data", (data) => { + const text = data.toString(); + stdout += text; + + // Stream output to console, converting TAP to readable format + for (const line of text.split("\n")) { + // Capture test result + const okMatch = line.match(/^\s*ok \d+ - (.+)/); + const notOkMatch = line.match(/^\s*not ok \d+ - (.+)/); + + if (okMatch) { + pendingTest = { name: okMatch[1], passed: true }; + } else if (notOkMatch) { + pendingTest = { name: notOkMatch[1], passed: false }; + } + + // When we see type: 'test', print the pending test + if ((line.includes("type: 'test'") || line.includes('type: "test"')) && pendingTest) { + const key = `${pendingTest.name}-${pendingTest.passed}`; + if (!printedTests.has(key)) { + printedTests.add(key); + if (pendingTest.passed) { + console.log(`✓ ${pendingTest.name}`); + } else { + console.log(`✗ ${pendingTest.name}`); + } + } + pendingTest = null; + } + } + }); + + child.stderr?.on("data", (data) => { + stderr += data.toString(); + process.stderr.write(data); + }); + + child.on("close", () => { + const { passed, failed, errors } = parseTapOutput(stdout); + resolve({ + platform: "node", + passed, + failed, + errors, + }); + }); + + child.on("error", (err) => { + resolve({ + platform: "node", + passed: 0, + failed: 1, + errors: [{ name: "spawn error", error: err.message }], + }); + }); + }); +} + +/** + * Parse Bun test output to extract test results + * Format: "N pass", "N fail" + */ +function parseBunOutput(output: string): { passed: number; failed: number; errors: Array<{ name: string; error: string }> } { + let passed = 0; + let failed = 0; + const errors: Array<{ name: string; error: string }> = []; + + // Match "N pass" and "N fail" lines + const passMatch = output.match(/^\s*(\d+)\s+pass/m); + const failMatch = output.match(/^\s*(\d+)\s+fail/m); + + if (passMatch) passed = parseInt(passMatch[1], 10); + if (failMatch) failed = parseInt(failMatch[1], 10); + + return { passed, failed, errors }; +} + +/** + * Run tests in Bun + */ +async function runBunTests(bundlePath: string, timeout: number): Promise { + const { spawn } = await import("child_process"); + + return new Promise((resolve) => { + const child = spawn("bun", ["test", bundlePath], { + stdio: ["pipe", "pipe", "pipe"], + timeout, + }); + + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + const text = data.toString(); + stdout += text; + process.stdout.write(data); + }); + + child.stderr?.on("data", (data) => { + const text = data.toString(); + stderr += text; + process.stderr.write(data); + }); + + child.on("close", () => { + const { passed, failed, errors } = parseBunOutput(stdout + stderr); + resolve({ + platform: "bun", + passed, + failed, + errors, + }); + }); + + child.on("error", (err) => { + resolve({ + platform: "bun", + passed: 0, + failed: 1, + errors: [{ name: "spawn error", error: err.message }], + }); + }); + }); +} + +/** + * Run tests in browser using Playwright + */ +async function runBrowserTests( + bundlePath: string, + browser: "chromium" | "firefox" | "webkit", + timeout: number, + debug: boolean, + cwd: string +): Promise { + // Try to import playwright from the test project's node_modules + let playwright: typeof import("playwright"); + try { + // Create a require function that resolves from the test project + const require = createRequire(Path.join(cwd, "package.json")); + playwright = require("playwright"); + } catch { + console.error("Playwright is required for browser tests."); + console.error("Install it with: npm install -D playwright"); + return { + platform: `browser (${browser})`, + passed: 0, + failed: 1, + errors: [{ name: "setup", error: "Playwright not installed" }], + }; + } + + const bundleContent = await FS.readFile(bundlePath, "utf-8"); + + // Create a simple HTTP server to serve the test + const html = ` + + + + libuild tests + + + + +`; + + let server: Server; + let port: number; + + await new Promise((resolve) => { + server = createServer((req, res) => { + res.setHeader("Content-Type", "text/html"); + res.end(html); + }); + server.listen(0, () => { + const addr = server.address(); + port = typeof addr === "object" && addr ? addr.port : 3000; + resolve(); + }); + }); + + try { + const browserInstance = await playwright[browser].launch({ + headless: !debug, + }); + + const context = await browserInstance.newContext(); + const page = await context.newPage(); + + // Capture console output + page.on("console", (msg) => { + const type = msg.type(); + const text = msg.text(); + if (type === "error") { + console.error(text); + } else { + console.log(text); + } + }); + + // Capture page errors + page.on("pageerror", (err) => { + console.error("Page error:", err.message); + }); + + await page.goto(`http://localhost:${port}/`); + + // Wait for tests to complete + await page.waitForFunction( + () => (globalThis as any).__LIBUILD_TEST__?.ended === true, + { timeout } + ); + + // Get results + const results = await page.evaluate(() => (globalThis as any).__LIBUILD_TEST__); + + if (!debug) { + await browserInstance.close(); + } else { + console.log("\nDebug mode: browser left open. Press Ctrl+C to exit."); + await new Promise(() => {}); // Wait forever + } + + return { + platform: `browser (${browser})`, + passed: results.passed, + failed: results.failed, + errors: results.errors, + }; + } finally { + server!.close(); + } +} + +/** + * Print test results summary + */ +function printResults(results: TestResult[]): boolean { + console.log("\n" + "=".repeat(60)); + console.log("Test Results Summary"); + console.log("=".repeat(60)); + + let allPassed = true; + + for (const result of results) { + const status = result.failed === 0 ? "✓" : "✗"; + const color = result.failed === 0 ? "\x1b[32m" : "\x1b[31m"; + const reset = "\x1b[0m"; + + console.log( + `${color}${status}${reset} ${result.platform}: ${result.passed} passed, ${result.failed} failed` + ); + + if (result.failed > 0) { + allPassed = false; + for (const error of result.errors) { + console.log(` ✗ ${error.name}`); + console.log(` ${error.error}`); + } + } + } + + console.log("=".repeat(60)); + + return allPassed; +} + +/** + * Main test runner + */ +export async function runTests(options: Partial = {}): Promise { + const opts: TestRunnerOptions = { + cwd: options.cwd || process.cwd(), + patterns: options.patterns || DEFAULT_PATTERNS, + platforms: options.platforms || ["bun"], + debug: options.debug || false, + timeout: options.timeout || 60000, + watch: options.watch || false, + }; + + console.log("Finding test files..."); + const testFiles = await findTestFiles(opts.cwd, opts.patterns); + + if (testFiles.length === 0) { + console.log("No test files found."); + return true; + } + + console.log(`Found ${testFiles.length} test file(s)`); + + // Create temp directory for bundles + const tempDir = Path.join(opts.cwd, ".libuild-test"); + await FS.mkdir(tempDir, { recursive: true }); + + const results: TestResult[] = []; + + try { + for (const platform of opts.platforms) { + console.log(`\nBuilding tests for ${platform}...`); + const bundlePath = await bundleTests(testFiles, platform, tempDir, opts.cwd); + + console.log(`Running tests on ${platform}...`); + + let result: TestResult; + if (platform === "bun") { + result = await runBunTests(bundlePath, opts.timeout); + } else if (platform === "node") { + result = await runNodeTests(bundlePath, opts.timeout); + } else { + // Browser platforms: chromium, firefox, webkit + result = await runBrowserTests(bundlePath, platform, opts.timeout, opts.debug, opts.cwd); + } + + results.push(result); + } + + return printResults(results); + } finally { + // Clean up temp directory (unless in debug mode) + if (!opts.debug) { + await FS.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } + } +} + +/** + * Detect available platforms + */ +export async function detectPlatforms(): Promise<("bun" | "node" | "browser")[]> { + const platforms: ("bun" | "node" | "browser")[] = []; + + // Check for Bun + const { spawn } = await import("child_process"); + try { + await new Promise((resolve, reject) => { + const child = spawn("bun", ["--version"], { stdio: "ignore" }); + child.on("close", (code) => (code === 0 ? resolve() : reject())); + child.on("error", reject); + }); + platforms.push("bun"); + } catch {} + + // Node is always available (we're running in it) + platforms.push("node"); + + // Check for Playwright + try { + await import("playwright"); + platforms.push("browser"); + } catch {} + + return platforms; +} diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..ff767b5 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,34 @@ +/** + * @b9g/libuild/test - Cross-platform test utilities + * + * Provides a unified testing API across Bun, Node, and browsers. + * Uses top-level await to detect runtime and load appropriate shim. + */ + +declare const Bun: unknown; + +const isBun = typeof Bun !== "undefined"; + +const { + describe, + test, + it, + expect, + beforeAll, + afterAll, + beforeEach, + afterEach, +} = isBun + ? await import("./test-bun.js") + : await import("./test-node.js"); + +export { + describe, + test, + it, + expect, + beforeAll, + afterAll, + beforeEach, + afterEach, +};