From eb6aa4049f9e12a831eec40cc5f867a4f5f08e92 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Thu, 22 Oct 2026 07:28:39 -0400 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=93=A6=20Prepare=20Babel=20for=20comp?= =?UTF-8?q?onent=20module=20adapters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deno.lock | 282 ++++++++++++++++++++++++++++--------- package.json | 5 +- packages/core/deno.json | 3 +- packages/core/package.json | 3 +- 4 files changed, 222 insertions(+), 71 deletions(-) diff --git a/deno.lock b/deno.lock index ef0eaacd5..c55162f1d 100644 --- a/deno.lock +++ b/deno.lock @@ -41,6 +41,7 @@ "npm:@agentclientprotocol/sdk@1.3.0": "1.3.0_zod@4.6.6", "npm:@babel/core@^7.28.0": "7.29.7", "npm:@babel/preset-react@^7.27.1": "7.29.7_@babel+core@7.29.7", + "npm:@babel/standalone@8.0.7": "8.0.7", "npm:@bomb.sh/tty@0.9.0": "0.9.0", "npm:@durable-streams/client@~0.2.2": "0.2.6", "npm:@durable-streams/server@~0.3.8": "0.3.8", @@ -292,26 +293,36 @@ "@babel/code-frame@7.29.7": { "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dependencies": [ - "@babel/helper-validator-identifier", - "js-tokens", + "@babel/helper-validator-identifier@7.29.7", + "js-tokens@4.0.0", "picocolors" ] }, + "@babel/code-frame@8.0.6": { + "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", + "dependencies": [ + "@babel/helper-validator-identifier@8.0.6", + "js-tokens@10.0.0" + ] + }, "@babel/compat-data@7.29.7": { "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==" }, + "@babel/compat-data@8.0.5": { + "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==" + }, "@babel/core@7.29.7": { "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dependencies": [ - "@babel/code-frame", - "@babel/generator", - "@babel/helper-compilation-targets", + "@babel/code-frame@7.29.7", + "@babel/generator@7.29.7", + "@babel/helper-compilation-targets@7.29.7", "@babel/helper-module-transforms", - "@babel/helpers", - "@babel/parser", - "@babel/template", - "@babel/traverse", - "@babel/types", + "@babel/helpers@7.29.7", + "@babel/parser@7.29.7", + "@babel/template@7.29.7", + "@babel/traverse@7.29.7", + "@babel/types@7.29.7", "@jridgewell/remapping", "convert-source-map", "debug", @@ -320,49 +331,94 @@ "semver@6.3.1" ] }, + "@babel/core@8.0.7": { + "integrity": "sha512-ZIsZWBG6Y1oa+8fkQVp0xeCdjCHBkXFCCIWrmg9SvejVvfcVQgKKRmGrBDWfM7rilhvnRoEuaqa5dkK7KB3pHA==", + "dependencies": [ + "@babel/code-frame@8.0.6", + "@babel/generator@8.0.6", + "@babel/helper-compilation-targets@8.0.7", + "@babel/helpers@8.0.7", + "@babel/parser@8.0.7", + "@babel/template@8.0.0", + "@babel/traverse@8.0.7", + "@babel/types@8.0.6", + "@types/gensync", + "convert-source-map", + "empathic", + "gensync", + "import-meta-resolve", + "json5", + "obug", + "verkit" + ] + }, "@babel/generator@7.29.7": { "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dependencies": [ - "@babel/parser", - "@babel/types", - "@jridgewell/gen-mapping", + "@babel/parser@7.29.7", + "@babel/types@7.29.7", + "@jridgewell/gen-mapping@0.3.13", + "@jridgewell/trace-mapping", + "jsesc" + ] + }, + "@babel/generator@8.0.6": { + "integrity": "sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==", + "dependencies": [ + "@babel/parser@8.0.7", + "@babel/types@8.0.6", + "@jridgewell/gen-mapping@0.4.0-beta.0", "@jridgewell/trace-mapping", + "@types/jsesc", "jsesc" ] }, "@babel/helper-annotate-as-pure@7.29.7": { "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dependencies": [ - "@babel/types" + "@babel/types@7.29.7" ] }, "@babel/helper-compilation-targets@7.29.7": { "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dependencies": [ - "@babel/compat-data", - "@babel/helper-validator-option", + "@babel/compat-data@7.29.7", + "@babel/helper-validator-option@7.29.7", "browserslist", "lru-cache", "semver@6.3.1" ] }, + "@babel/helper-compilation-targets@8.0.7": { + "integrity": "sha512-ilAZCdOOlOVA7dQbaFNw2f/59p6GW33NxTopc3DvEE3efp3ZXTrGX/yZScaKw8/vd9bq31Ag0EcXiLw32bAgJg==", + "dependencies": [ + "@babel/compat-data@8.0.5", + "@babel/helper-validator-option@8.0.0", + "browserslist", + "flru", + "verkit" + ] + }, "@babel/helper-globals@7.29.7": { "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==" }, + "@babel/helper-globals@8.0.6": { + "integrity": "sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==" + }, "@babel/helper-module-imports@7.29.7": { "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dependencies": [ - "@babel/traverse", - "@babel/types" + "@babel/traverse@7.29.7", + "@babel/types@7.29.7" ] }, "@babel/helper-module-transforms@7.29.7_@babel+core@7.29.7": { "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@babel/helper-module-imports", - "@babel/helper-validator-identifier", - "@babel/traverse" + "@babel/helper-validator-identifier@7.29.7", + "@babel/traverse@7.29.7" ] }, "@babel/helper-plugin-utils@7.29.7": { @@ -371,62 +427,85 @@ "@babel/helper-string-parser@7.29.7": { "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" }, + "@babel/helper-string-parser@8.0.6": { + "integrity": "sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==" + }, "@babel/helper-validator-identifier@7.29.7": { "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" }, + "@babel/helper-validator-identifier@8.0.6": { + "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==" + }, "@babel/helper-validator-option@7.29.7": { "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==" }, + "@babel/helper-validator-option@8.0.0": { + "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==" + }, "@babel/helpers@7.29.7": { "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dependencies": [ - "@babel/template", - "@babel/types" + "@babel/template@7.29.7", + "@babel/types@7.29.7" + ] + }, + "@babel/helpers@8.0.7": { + "integrity": "sha512-5dMj5kFwS2AY7Dt4PZp4px3c5ThMbbEa7tZfuHTncCGSFYq8uoA1ufDE1IS/lYpQedE+3+GQ3cPZUiplIu195w==", + "dependencies": [ + "@babel/template@8.0.0", + "@babel/types@8.0.6" ] }, "@babel/parser@7.29.7": { "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dependencies": [ - "@babel/types" + "@babel/types@7.29.7" + ], + "bin": true + }, + "@babel/parser@8.0.7": { + "integrity": "sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==", + "dependencies": [ + "@babel/types@8.0.6" ], "bin": true }, "@babel/plugin-syntax-jsx@7.29.7_@babel+core@7.29.7": { "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@babel/helper-plugin-utils" ] }, "@babel/plugin-transform-react-display-name@7.29.7_@babel+core@7.29.7": { "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@babel/helper-plugin-utils" ] }, "@babel/plugin-transform-react-jsx-development@7.29.7_@babel+core@7.29.7": { "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@babel/plugin-transform-react-jsx" ] }, "@babel/plugin-transform-react-jsx@7.29.7_@babel+core@7.29.7": { "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@babel/helper-annotate-as-pure", "@babel/helper-module-imports", "@babel/helper-plugin-utils", "@babel/plugin-syntax-jsx", - "@babel/types" + "@babel/types@7.29.7" ] }, "@babel/plugin-transform-react-pure-annotations@7.29.7_@babel+core@7.29.7": { "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@babel/helper-annotate-as-pure", "@babel/helper-plugin-utils" ] @@ -434,40 +513,75 @@ "@babel/preset-react@7.29.7_@babel+core@7.29.7": { "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@babel/helper-plugin-utils", - "@babel/helper-validator-option", + "@babel/helper-validator-option@7.29.7", "@babel/plugin-transform-react-display-name", "@babel/plugin-transform-react-jsx", "@babel/plugin-transform-react-jsx-development", "@babel/plugin-transform-react-pure-annotations" ] }, + "@babel/standalone@8.0.7": { + "integrity": "sha512-Dz1O7HeZW2YvQ/NPDXQZ90NKp0MnQ1ed3a2FqXoi8ikkOMS4VIcE1Ouldp2S6gTatMZqqrTsR8om21B3Umrh4w==", + "dependencies": [ + "@babel/core@8.0.7", + "@babel/generator@8.0.6", + "@babel/parser@8.0.7" + ] + }, "@babel/template@7.29.7": { "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dependencies": [ - "@babel/code-frame", - "@babel/parser", - "@babel/types" + "@babel/code-frame@7.29.7", + "@babel/parser@7.29.7", + "@babel/types@7.29.7" + ] + }, + "@babel/template@8.0.0": { + "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", + "dependencies": [ + "@babel/code-frame@8.0.6", + "@babel/parser@8.0.7", + "@babel/types@8.0.6" ] }, "@babel/traverse@7.29.7": { "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dependencies": [ - "@babel/code-frame", - "@babel/generator", - "@babel/helper-globals", - "@babel/parser", - "@babel/template", - "@babel/types", + "@babel/code-frame@7.29.7", + "@babel/generator@7.29.7", + "@babel/helper-globals@7.29.7", + "@babel/parser@7.29.7", + "@babel/template@7.29.7", + "@babel/types@7.29.7", "debug" ] }, + "@babel/traverse@8.0.7": { + "integrity": "sha512-bVV0brinsTWgc6ptSIoQo3GbITtl0VZAqEUJbHHcyfDPEu+q/fEvlYEa+6H85HoK8IdksKBFtKJbJFZ6Kff+zg==", + "dependencies": [ + "@babel/code-frame@8.0.6", + "@babel/generator@8.0.6", + "@babel/helper-globals@8.0.6", + "@babel/parser@8.0.7", + "@babel/template@8.0.0", + "@babel/types@8.0.6", + "obug" + ] + }, "@babel/types@7.29.7": { "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dependencies": [ - "@babel/helper-string-parser", - "@babel/helper-validator-identifier" + "@babel/helper-string-parser@7.29.7", + "@babel/helper-validator-identifier@7.29.7" + ] + }, + "@babel/types@8.0.6": { + "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==", + "dependencies": [ + "@babel/helper-string-parser@8.0.6", + "@babel/helper-validator-identifier@8.0.6" ] }, "@bomb.sh/tty@0.9.0": { @@ -1093,18 +1207,25 @@ "@jridgewell/trace-mapping" ] }, + "@jridgewell/gen-mapping@0.4.0-beta.0": { + "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, "@jridgewell/remapping@2.3.5": { "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dependencies": [ - "@jridgewell/gen-mapping", + "@jridgewell/gen-mapping@0.3.13", "@jridgewell/trace-mapping" ] }, "@jridgewell/resolve-uri@3.1.2": { "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" }, - "@jridgewell/sourcemap-codec@1.5.5": { - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + "@jridgewell/sourcemap-codec@1.6.0": { + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==" }, "@jridgewell/trace-mapping@0.3.31": { "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", @@ -1637,7 +1758,7 @@ "@prefresh/vite@2.4.12_preact@10.29.7_vite@7.3.6_preact-render-to-string@6.7.0": { "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", "dependencies": [ - "@babel/core", + "@babel/core@7.29.7", "@prefresh/babel-plugin", "@prefresh/core", "@prefresh/utils", @@ -2335,8 +2456,8 @@ "@types/babel__core@7.20.5": { "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": [ - "@babel/parser", - "@babel/types", + "@babel/parser@7.29.7", + "@babel/types@7.29.7", "@types/babel__generator", "@types/babel__template", "@types/babel__traverse" @@ -2345,20 +2466,20 @@ "@types/babel__generator@7.27.0": { "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dependencies": [ - "@babel/types" + "@babel/types@7.29.7" ] }, "@types/babel__template@7.4.4": { "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dependencies": [ - "@babel/parser", - "@babel/types" + "@babel/parser@7.29.7", + "@babel/types@7.29.7" ] }, "@types/babel__traverse@7.28.0": { "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dependencies": [ - "@babel/types" + "@babel/types@7.29.7" ] }, "@types/debug@4.1.12": { @@ -2370,6 +2491,9 @@ "@types/estree@1.0.9": { "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" }, + "@types/gensync@1.0.5": { + "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==" + }, "@types/hast@3.0.4": { "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": [ @@ -2391,6 +2515,9 @@ "@types/istanbul-lib-report" ] }, + "@types/jsesc@2.5.1": { + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==" + }, "@types/json-schema@7.0.15": { "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, @@ -2551,8 +2678,8 @@ "bare-path" ] }, - "baseline-browser-mapping@2.11.1": { - "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "baseline-browser-mapping@2.11.30": { + "integrity": "sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==", "bin": true }, "boolbase@1.0.0": { @@ -2561,8 +2688,8 @@ "boundary@2.0.0": { "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==" }, - "browserslist@4.28.7": { - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "browserslist@4.29.4": { + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", "dependencies": [ "baseline-browser-mapping", "caniuse-lite", @@ -2572,8 +2699,8 @@ ], "bin": true }, - "caniuse-lite@1.0.30001806": { - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==" + "caniuse-lite@1.0.30001818": { + "integrity": "sha512-XJbgo4adscw7R4MZU4/qVFkoMyPaNUAWmdNY4o/k9x7fFwmv4frIlndI9lWuaxviW9ObiZel9LuncnQPn6hQ5A==" }, "ccount@2.0.1": { "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" @@ -2721,8 +2848,8 @@ "effection@4.1.0": { "integrity": "sha512-/BJuaYhzDrvC/nrIW054z1itmCNjVNFtyv/J26xrS4pP2MLfm5TG8VFe5f4zl1oGcAoF1fq1GfDO+v0qx6URPw==" }, - "electron-to-chromium@1.5.396": { - "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==" + "electron-to-chromium@1.5.463": { + "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==" }, "emoji-regex@8.0.0": { "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" @@ -2730,6 +2857,9 @@ "emojilib@2.4.0": { "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" }, + "empathic@2.1.0": { + "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==" + }, "enhanced-resolve@5.24.3": { "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "dependencies": [ @@ -2933,6 +3063,9 @@ "picomatch@4.0.7" ] }, + "flru@1.0.2": { + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==" + }, "formatly@0.11.4": { "integrity": "sha512-TeCC10lP8KCgoX+Fe5lryILIT0/Dy+nP0OSVnsuB5CwXoLuOdqxCYlPFF1inR8S/zTy1ec7TjhS2TeRKWzzImA==", "dependencies": [ @@ -3015,6 +3148,9 @@ "immutable@5.1.5": { "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==" }, + "import-meta-resolve@4.2.0": { + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==" + }, "is-extendable@0.1.1": { "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" }, @@ -3048,7 +3184,7 @@ "jest-message-util@30.3.0": { "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", "dependencies": [ - "@babel/code-frame", + "@babel/code-frame@7.29.7", "@jest/types", "@types/stack-utils", "chalk@4.1.2", @@ -3085,6 +3221,9 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "bin": true }, + "js-tokens@10.0.0": { + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==" + }, "js-tokens@4.0.0": { "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, @@ -3239,7 +3378,7 @@ "loose-envify@1.4.0": { "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dependencies": [ - "js-tokens" + "js-tokens@4.0.0" ], "bin": true }, @@ -3570,8 +3709,8 @@ ], "bin": true }, - "node-releases@2.0.51": { - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==" + "node-releases@2.0.59": { + "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==" }, "nth-check@2.1.1": { "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", @@ -3582,6 +3721,9 @@ "object-assign@4.1.1": { "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "obug@3.0.0": { + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==" + }, "ordered-binary@1.6.1": { "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==" }, @@ -4193,8 +4335,8 @@ "unist-util-visit-parents" ] }, - "update-browserslist-db@1.2.3_browserslist@4.28.7": { - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "update-browserslist-db@1.3.4_browserslist@4.29.4": { + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "dependencies": [ "browserslist", "escalade", @@ -4221,6 +4363,9 @@ "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", "bin": true }, + "verkit@0.5.0": { + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==" + }, "vfile-message@4.0.3": { "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dependencies": [ @@ -4349,6 +4494,7 @@ ], "packageJson": { "dependencies": [ + "npm:@babel/standalone@8.0.7", "npm:@durable-streams/client@~0.2.2", "npm:@durable-streams/server@~0.3.8", "npm:@effectionx/context-api@0.6.0", @@ -4429,6 +4575,7 @@ }, "packages/core": { "dependencies": [ + "npm:@babel/standalone@8.0.7", "npm:@effectionx/context-api@0.6.0", "npm:@secretlint/core@13.0.4", "npm:@secretlint/profiler@13.0.4", @@ -4449,6 +4596,7 @@ ], "packageJson": { "dependencies": [ + "npm:@babel/standalone@8.0.7", "npm:@effectionx/context-api@0.6.0", "npm:@effectionx/converge@0.1.4", "npm:@effectionx/fetch@0.2.1", diff --git a/package.json b/package.json index 754c8c650..5278159c7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "type": "module", - "description": "executable.md — treat markdown documents as executable workflows.", + "description": "executable.md \u2014 treat markdown documents as executable workflows.", "homepage": "https://executable.md", "repository": { "type": "git", @@ -47,7 +47,8 @@ "semver": "^7.8.5", "zod": "^4.3.6", "unist-util-select": "^5", - "mdast-util-to-string": "^4" + "mdast-util-to-string": "^4", + "@babel/standalone": "8.0.7" }, "devDependencies": { "@durable-streams/server": "^0.3.8", diff --git a/packages/core/deno.json b/packages/core/deno.json index 955fa0756..0482b6f48 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -23,6 +23,7 @@ "unist-util-select": "npm:unist-util-select@^5", "mdast-util-to-string": "npm:mdast-util-to-string@^4", "@secretlint/profiler": "npm:@secretlint/profiler@13.0.4", - "yq.rs": "npm:yq.rs@0.1.4" + "yq.rs": "npm:yq.rs@0.1.4", + "@babel/standalone": "npm:@babel/standalone@8.0.7" } } diff --git a/packages/core/package.json b/packages/core/package.json index 4f76bbaf6..a341d0e30 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,6 +35,7 @@ "remark": "15", "remend": "^1.2.2", "unist-util-select": "^5", - "yq.rs": "0.1.4" + "yq.rs": "0.1.4", + "@babel/standalone": "8.0.7" } } From c195b20e7e02c1364c13f2581232aa0d5c6c6b05 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Thu, 22 Oct 2026 07:36:52 -0400 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=A6=20Prepare=20explicit=20compone?= =?UTF-8?q?nt=20module=20host=20exports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deno.json | 8 +- packages/core/deno.json | 5 +- packages/core/package.json | 5 +- packages/test-support/package.json | 6 +- pnpm-lock.yaml | 298 +++++++++++++++++++++++++++++ 5 files changed, 316 insertions(+), 6 deletions(-) diff --git a/deno.json b/deno.json index 2219f7c1e..3b4b18680 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,8 @@ { - "workspace": ["packages/*", "site"], + "workspace": [ + "packages/*", + "site" + ], "exclude": [ "scripts/tests/fixtures", ".xmd-eval", @@ -50,7 +53,8 @@ "unist-util-select": "npm:unist-util-select@^5", "zod": "npm:zod@^4.3.6", "mdast-util-to-string": "npm:mdast-util-to-string@^4", - "yq.rs": "npm:yq.rs@0.1.4" + "yq.rs": "npm:yq.rs@0.1.4", + "@executablemd/test-support/component-modules": "./packages/test-support/component-modules.ts" }, "tasks": { "xmd": "deno run --allow-all packages/cli/src/deno.ts", diff --git a/packages/core/deno.json b/packages/core/deno.json index 0482b6f48..7b69fd6b9 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -4,7 +4,10 @@ "exports": { ".": "./mod.ts", "./api": "./api.ts", - "./host": "./host.ts" + "./host": "./host.ts", + "./component-modules/deno": "./component-modules/deno.ts", + "./component-modules/node": "./component-modules/node.ts", + "./component-modules/bun": "./component-modules/bun.ts" }, "imports": { "@effectionx/context-api": "npm:@effectionx/context-api@0.6.0", diff --git a/packages/core/package.json b/packages/core/package.json index a341d0e30..613bb2c2b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -6,7 +6,10 @@ "exports": { ".": "./mod.ts", "./api": "./api.ts", - "./host": "./host.ts" + "./host": "./host.ts", + "./component-modules/deno": "./component-modules/deno.ts", + "./component-modules/node": "./component-modules/node.ts", + "./component-modules/bun": "./component-modules/bun.ts" }, "dependencies": { "@effectionx/context-api": "0.6.0", diff --git a/packages/test-support/package.json b/packages/test-support/package.json index 09e5dd014..574ae9c0f 100644 --- a/packages/test-support/package.json +++ b/packages/test-support/package.json @@ -9,7 +9,8 @@ "./host-boundary": "./host-boundary.ts", "./journal": "./journal.ts", "./launch": "./launch.ts", - "./temp": "./temp.ts" + "./temp": "./temp.ts", + "./component-modules": "./component-modules.ts" }, "dependencies": { "@effectionx/fs": "0.3.0", @@ -18,6 +19,7 @@ "@effectionx/timebox": "0.4.3", "@executablemd/durable-streams": "workspace:*", "effection": "4.1.0", - "expect": "^30.0.0" + "expect": "^30.0.0", + "@executablemd/core": "workspace:*" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5aa5dc4c2..c7e2b012f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@babel/standalone': + specifier: 8.0.7 + version: 8.0.7 '@durable-streams/client': specifier: ^0.2.2 version: 0.2.2 @@ -238,6 +241,9 @@ importers: packages/core: dependencies: + '@babel/standalone': + specifier: 8.0.7 + version: 8.0.7 '@effectionx/context-api': specifier: 0.6.0 version: 0.6.0(effection@4.1.0) @@ -557,10 +563,71 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@8.0.6': + resolution: {integrity: sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/compat-data@8.0.5': + resolution: {integrity: sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/core@8.0.7': + resolution: {integrity: sha512-ZIsZWBG6Y1oa+8fkQVp0xeCdjCHBkXFCCIWrmg9SvejVvfcVQgKKRmGrBDWfM7rilhvnRoEuaqa5dkK7KB3pHA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/generator@8.0.6': + resolution: {integrity: sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-compilation-targets@8.0.7': + resolution: {integrity: sha512-ilAZCdOOlOVA7dQbaFNw2f/59p6GW33NxTopc3DvEE3efp3ZXTrGX/yZScaKw8/vd9bq31Ag0EcXiLw32bAgJg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-globals@8.0.6': + resolution: {integrity: sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-string-parser@8.0.6': + resolution: {integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.6': + resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-option@8.0.0': + resolution: {integrity: sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helpers@8.0.7': + resolution: {integrity: sha512-5dMj5kFwS2AY7Dt4PZp4px3c5ThMbbEa7tZfuHTncCGSFYq8uoA1ufDE1IS/lYpQedE+3+GQ3cPZUiplIu195w==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@8.0.7': + resolution: {integrity: sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/standalone@8.0.7': + resolution: {integrity: sha512-Dz1O7HeZW2YvQ/NPDXQZ90NKp0MnQ1ed3a2FqXoi8ikkOMS4VIcE1Ouldp2S6gTatMZqqrTsR8om21B3Umrh4w==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/template@8.0.0': + resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/traverse@8.0.7': + resolution: {integrity: sha512-bVV0brinsTWgc6ptSIoQo3GbITtl0VZAqEUJbHHcyfDPEu+q/fEvlYEa+6H85HoK8IdksKBFtKJbJFZ6Kff+zg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/types@8.0.6': + resolution: {integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==} + engines: {node: ^22.18.0 || >=24.11.0} + '@bomb.sh/tty@0.9.0': resolution: {integrity: sha512-1fX9lgwdc+kGRQeVEYwv7cJb5i855ysvB/TMCC3/wnnjMiKLZjWew6LLnsAMq4pLzkweivYw+zwYNZRUVhjoBA==} engines: {node: '>= 22'} @@ -1033,9 +1100,22 @@ packages: resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.4.0-beta.0': + resolution: {integrity: sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lmdb/lmdb-darwin-arm64@3.5.6': resolution: {integrity: sha512-mY5FG4TjPAkY4P0w+OhHaUka5mDh2TX2WKYIwuKzJ1zeW3VvRgxdam/lGJTquI+bthTx5CSHDW+BAQCnNAzkEA==} cpu: [arm64] @@ -1992,6 +2072,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/gensync@1.0.5': + resolution: {integrity: sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==} + '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} @@ -2004,6 +2087,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2136,12 +2222,25 @@ packages: bare-url@2.4.6: resolution: {integrity: sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==} + baseline-browser-mapping@2.11.30: + resolution: {integrity: sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==} + engines: {node: '>=6.0.0'} + hasBin: true + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} boundary@2.0.0: resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==} + browserslist@4.29.4: + resolution: {integrity: sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001819: + resolution: {integrity: sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2213,6 +2312,9 @@ packages: resolution: {integrity: sha512-4bxK3+L+FHr9Xm/d69Syvvpvkj7lj7a4zz3B+tchuohg5WKeudyBS+4Oob5Zdgoh8I7+n2lj0lfa8I6cUXfcEg==} engines: {node: '>= 16'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2253,12 +2355,19 @@ packages: resolution: {integrity: sha512-/BJuaYhzDrvC/nrIW054z1itmCNjVNFtyv/J26xrS4pP2MLfm5TG8VFe5f4zl1oGcAoF1fq1GfDO+v0qx6URPw==} engines: {node: '>= 16'} + electron-to-chromium@1.5.463: + resolution: {integrity: sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + empathic@2.1.0: + resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==} + engines: {node: '>=14'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2340,6 +2449,10 @@ packages: picomatch: optional: true + flru@1.0.2: + resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==} + engines: {node: '>=6'} + formatly@0.11.4: resolution: {integrity: sha512-TeCC10lP8KCgoX+Fe5lryILIT0/Dy+nP0OSVnsuB5CwXoLuOdqxCYlPFF1inR8S/zTy1ec7TjhS2TeRKWzzImA==} engines: {node: '>=18.14.0'} @@ -2350,6 +2463,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2393,6 +2510,9 @@ packages: immutable@5.1.5: resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -2436,6 +2556,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2443,9 +2566,19 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} @@ -2615,6 +2748,10 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + node-releases@2.0.60: + resolution: {integrity: sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -2622,6 +2759,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + ordered-binary@1.6.1: resolution: {integrity: sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==} @@ -2945,6 +3086,12 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -2969,6 +3116,10 @@ packages: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true + verkit@0.5.0: + resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} + engines: {node: '>=18.12.0'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -3041,8 +3192,95 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@8.0.6': + dependencies: + '@babel/helper-validator-identifier': 8.0.6 + js-tokens: 10.0.0 + + '@babel/compat-data@8.0.5': {} + + '@babel/core@8.0.7': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/generator': 8.0.6 + '@babel/helper-compilation-targets': 8.0.7 + '@babel/helpers': 8.0.7 + '@babel/parser': 8.0.7 + '@babel/template': 8.0.0 + '@babel/traverse': 8.0.7 + '@babel/types': 8.0.6 + '@types/gensync': 1.0.5 + convert-source-map: 2.0.0 + empathic: 2.1.0 + gensync: 1.0.0-beta.2 + import-meta-resolve: 4.2.0 + json5: 2.2.3 + obug: 3.0.0 + verkit: 0.5.0 + + '@babel/generator@8.0.6': + dependencies: + '@babel/parser': 8.0.7 + '@babel/types': 8.0.6 + '@jridgewell/gen-mapping': 0.4.0-beta.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@8.0.7': + dependencies: + '@babel/compat-data': 8.0.5 + '@babel/helper-validator-option': 8.0.0 + browserslist: 4.29.4 + flru: 1.0.2 + verkit: 0.5.0 + + '@babel/helper-globals@8.0.6': {} + + '@babel/helper-string-parser@8.0.6': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@8.0.6': {} + + '@babel/helper-validator-option@8.0.0': {} + + '@babel/helpers@8.0.7': + dependencies: + '@babel/template': 8.0.0 + '@babel/types': 8.0.6 + + '@babel/parser@8.0.7': + dependencies: + '@babel/types': 8.0.6 + + '@babel/standalone@8.0.7': + dependencies: + '@babel/core': 8.0.7 + '@babel/generator': 8.0.6 + '@babel/parser': 8.0.7 + + '@babel/template@8.0.0': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/parser': 8.0.7 + '@babel/types': 8.0.6 + + '@babel/traverse@8.0.7': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/generator': 8.0.6 + '@babel/helper-globals': 8.0.6 + '@babel/parser': 8.0.7 + '@babel/template': 8.0.0 + '@babel/types': 8.0.6 + obug: 3.0.0 + + '@babel/types@8.0.6': + dependencies: + '@babel/helper-string-parser': 8.0.6 + '@babel/helper-validator-identifier': 8.0.6 + '@bomb.sh/tty@0.9.0': {} '@clack/core@1.4.3': @@ -3371,8 +3609,22 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 + '@jridgewell/gen-mapping@0.4.0-beta.0': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@lmdb/lmdb-darwin-arm64@3.5.6': optional: true @@ -4038,6 +4290,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/gensync@1.0.5': {} + '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 @@ -4052,6 +4306,8 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jsesc@2.5.1': {} + '@types/json-schema@7.0.15': {} '@types/mdast@4.0.4': @@ -4164,10 +4420,22 @@ snapshots: dependencies: bare-path: 3.1.1 + baseline-browser-mapping@2.11.30: {} + boolbase@1.0.0: {} boundary@2.0.0: {} + browserslist@4.29.4: + dependencies: + baseline-browser-mapping: 2.11.30 + caniuse-lite: 1.0.30001819 + electron-to-chromium: 1.5.463 + node-releases: 2.0.60 + update-browserslist-db: 1.3.4(browserslist@4.29.4) + + caniuse-lite@1.0.30001819: {} + ccount@2.0.1: {} chalk@4.1.2: @@ -4240,6 +4508,8 @@ snapshots: dependencies: '@standard-schema/spec': 1.1.0 + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4270,10 +4540,14 @@ snapshots: effection@4.1.0: {} + electron-to-chromium@1.5.463: {} + emoji-regex@8.0.0: {} emojilib@2.4.0: {} + empathic@2.1.0: {} + environment@1.1.0: {} esbuild@0.27.4: @@ -4393,6 +4667,8 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + flru@1.0.2: {} + formatly@0.11.4: dependencies: fd-package-json: 2.0.0 @@ -4401,6 +4677,8 @@ snapshots: fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-nonce@1.0.1: {} @@ -4454,6 +4732,8 @@ snapshots: immutable@5.1.5: {} + import-meta-resolve@4.2.0: {} + is-extendable@0.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4507,6 +4787,8 @@ snapshots: jiti@2.7.0: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -4514,8 +4796,12 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + json5@2.2.3: {} + jsonpointer@5.0.1: {} kind-of@6.0.3: {} @@ -4807,12 +5093,16 @@ snapshots: dependencies: detect-libc: 2.1.2 + node-releases@2.0.60: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 object-assign@4.1.1: {} + obug@3.0.0: {} + ordered-binary@1.6.1: {} oxc-parser@0.153.0: @@ -5222,6 +5512,12 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.3.4(browserslist@4.29.4): + dependencies: + browserslist: 4.29.4 + escalade: 3.2.0 + picocolors: 1.1.1 + use-callback-ref@1.3.3(react@19.2.0): dependencies: react: 19.2.0 @@ -5235,6 +5531,8 @@ snapshots: uuid@14.0.1: {} + verkit@0.5.0: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 From 54b93ea41c10c82055880a948e261d57230addde Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Thu, 22 Oct 2026 07:59:19 -0400 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=90=9B=20Load=20TypeScript=20componen?= =?UTF-8?q?ts=20from=20selected=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture.md | 21 ++ knip.json | 27 +- packages/cli/src/bun.ts | 2 + packages/cli/src/cli.ts | 2 + packages/cli/src/compiled.ts | 2 + packages/cli/src/deno.ts | 2 + packages/cli/src/node.ts | 2 + packages/cli/src/testing-host.ts | 3 + .../testing-execution-host-native.test.ts | 46 +++ packages/core/api.ts | 1 + packages/core/component-modules/bun.ts | 46 +++ packages/core/component-modules/deno.ts | 30 ++ packages/core/component-modules/hooks.ts | 50 ++++ packages/core/component-modules/node.ts | 11 + packages/core/component-modules/transform.ts | 164 +++++++++++ packages/core/src/component-module-loader.ts | 12 + packages/core/src/execute.ts | 28 +- packages/core/src/typescript-module.ts | 47 +++ .../tests/component-module-loader.test.ts | 274 ++++++++++++++++++ .../core/tests/component-registration.test.ts | 2 + packages/core/tests/component-returns.test.ts | 3 + packages/core/tests/contextual-cwd.test.ts | 2 + .../fixtures/component-module-readonly.ts | 38 +++ .../fixtures/typescript-materialization.ts | 53 ++++ .../core/tests/function-components.test.ts | 2 + .../core/tests/generated-selection.test.ts | 2 + .../core/tests/invocation-identity.test.ts | 2 + .../tests/typescript-materialization.test.ts | 258 +++++++++++++++++ packages/test-support/component-modules.ts | 24 ++ packages/web/tests/component.test.ts | 2 + pnpm-lock.yaml | 3 + scripts/tests/cli-npm-bin.test.ts | 40 +++ scripts/tests/component-form-dispatch.test.ts | 23 +- specs/executable-mdx-spec.md | 51 +++- tsconfig.node.json | 82 ++---- 35 files changed, 1269 insertions(+), 88 deletions(-) create mode 100644 packages/core/component-modules/bun.ts create mode 100644 packages/core/component-modules/deno.ts create mode 100644 packages/core/component-modules/hooks.ts create mode 100644 packages/core/component-modules/node.ts create mode 100644 packages/core/component-modules/transform.ts create mode 100644 packages/core/src/component-module-loader.ts create mode 100644 packages/core/src/typescript-module.ts create mode 100644 packages/core/tests/component-module-loader.test.ts create mode 100644 packages/core/tests/fixtures/component-module-readonly.ts create mode 100644 packages/core/tests/fixtures/typescript-materialization.ts create mode 100644 packages/core/tests/typescript-materialization.test.ts create mode 100644 packages/test-support/component-modules.ts diff --git a/architecture.md b/architecture.md index 8ea01380e..0ac5b497b 100644 --- a/architecture.md +++ b/architecture.md @@ -201,6 +201,27 @@ hidden inside library objects that accumulate. One exception: metadata an author declares at module evaluation, about a value the author owns, may live on that value. +Ordinary TypeScript components use `ComponentModuleLoader`, the same-name +interface and scoped Context in `@executablemd/core/api`, with stable name +`@executablemd/core/ComponentModuleLoader` and default `undefined`. Canonical +setup captures its bound `load(source, origin)` before profile resolution. +Profile and durable imports pass exact selected direct bytes and the logical +file URL; core still validates the complete namespace and owns durable identity +and admission. Runtime-named CLI entrypoints install their matching core +`component-modules/deno`, `/node` or `/bun` provider; compiled installs Deno's. +Embeddings explicitly use the matching exported installer. Eval compilation is +a separate service. Absent installation refuses TypeScript loading before native +work; Markdown and registered functions are unaffected. + +These adapters transform direct TypeScript in memory, preserving module exports, +logical origin, source maps and ordinary live dependencies without staging files +or permission grants. Deno and Node retain owned native hooks; Bun retains an +owned JavaScript object URL. Loading children belong to canonical execution, +survive the short-lived materialization task and namespace return, and end after +later module use. Cancellation joins native import work before deregistration +or revocation. Retained imports execute retained direct text after file removal; +they do not snapshot dependencies or repeat completed effects. + A callback registered with something outside the process — a listener on a socket, a child process, a stream or a DOM target — is state of exactly this kind, and the source it is attached to knows nothing about the operation that diff --git a/knip.json b/knip.json index e7c33dfe7..a2e565f85 100644 --- a/knip.json +++ b/knip.json @@ -1,12 +1,6 @@ { "$schema": "https://unpkg.com/knip@6/schema.json", - "ignore": [ - "**/vendor/**", - "**/npm/**", - "scripts/tests/fixtures/**", - "**/_fresh/**", - "**/*.d.ts" - ], + "ignore": ["**/vendor/**", "**/npm/**", "scripts/tests/fixtures/**", "**/_fresh/**", "**/*.d.ts"], "workspaces": { ".": { "entry": [ @@ -36,6 +30,9 @@ ], "@executablemd/core": ["packages/core/mod.ts"], "@executablemd/core/api": ["packages/core/api.ts"], + "@executablemd/core/component-modules/deno": ["packages/core/component-modules/deno.ts"], + "@executablemd/core/component-modules/node": ["packages/core/component-modules/node.ts"], + "@executablemd/core/component-modules/bun": ["packages/core/component-modules/bun.ts"], "@executablemd/core/host": ["packages/core/host.ts"], "@executablemd/durable-streams": ["packages/durable-streams/mod.ts"], "@executablemd/git": ["packages/git/mod.ts"], @@ -66,12 +63,7 @@ "demo/**/*.{ts,tsx}", "examples/**/*.{ts,tsx}" ], - "project": [ - "**/*.{ts,tsx,js,jsx}", - "!vendor/**", - "!src/repl/vendor/**", - "!npm/**" - ], + "project": ["**/*.{ts,tsx,js,jsx}", "!vendor/**", "!src/repl/vendor/**", "!npm/**"], "paths": { "@executablemd/acp": ["../../packages/acp/mod.ts"], "@executablemd/acp/embedded-adapters": ["../../packages/acp/embedded-adapters.ts"], @@ -82,6 +74,15 @@ ], "@executablemd/core": ["../../packages/core/mod.ts"], "@executablemd/core/api": ["../../packages/core/api.ts"], + "@executablemd/core/component-modules/deno": [ + "../../packages/core/component-modules/deno.ts" + ], + "@executablemd/core/component-modules/node": [ + "../../packages/core/component-modules/node.ts" + ], + "@executablemd/core/component-modules/bun": [ + "../../packages/core/component-modules/bun.ts" + ], "@executablemd/core/host": ["../../packages/core/host.ts"], "@executablemd/durable-streams": ["../../packages/durable-streams/mod.ts"], "@executablemd/git": ["../../packages/git/mod.ts"], diff --git a/packages/cli/src/bun.ts b/packages/cli/src/bun.ts index bd7d40bf8..809a5813d 100644 --- a/packages/cli/src/bun.ts +++ b/packages/cli/src/bun.ts @@ -11,6 +11,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileTempFile } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; +import { useBunComponentModuleLoader } from "@executablemd/core/component-modules/bun"; import { useBunRepl } from "./bun-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; @@ -36,6 +37,7 @@ const UPGRADE: UpgradeAssembly = { }; await main(function* (args) { + yield* useBunComponentModuleLoader(); // The base providers for this host. `at: "min"` puts them beneath ordinary // middleware, so middleware installed later can wrap either one. yield* API.Env.around( diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index afb9c0d79..a8140b8d9 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -154,6 +154,7 @@ import { selectPlugins } from "./plugin-selection.ts"; import type { PluginSelection } from "./plugin-selection.ts"; import { deliverWhole } from "./stdout-delivery.ts"; import { testingExecutionHost } from "./testing-host.ts"; +import { ComponentModuleLoader } from "@executablemd/core/api"; import type { ChildPlanDeclaration } from "./testing-host.ts"; import { EVAL_ALIAS, EVAL_OPTION, evalGrammarError, readEvalFlags } from "./eval-source.ts"; import type { EvalFlags } from "./eval-source.ts"; @@ -1436,6 +1437,7 @@ function* runDocument( // that inherits no `API.Env` handler. What crosses is the argv, not the Api // that produced it. const testingHost = testingExecutionHost({ + componentModuleLoader: yield* ComponentModuleLoader.get(), includes: include, // The Plugin *values*, so an isolated child installs them again for itself // with command `run`. Live middleware and installation-scoped resources diff --git a/packages/cli/src/compiled.ts b/packages/cli/src/compiled.ts index 32af929b1..3c07fa10c 100644 --- a/packages/cli/src/compiled.ts +++ b/packages/cli/src/compiled.ts @@ -10,6 +10,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileDataUri } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; +import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; import { useCompiledRepl } from "./compiled-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; @@ -54,6 +55,7 @@ if (isCredentialHelperMode(process.argv.slice(2))) { await main(() => runCredentialHelper(process.argv.slice(2))); } else { await main(function* (args) { + yield* useDenoComponentModuleLoader(); // The base providers for this host. `at: "min"` puts them beneath ordinary // middleware, so middleware installed later can wrap either one. yield* API.Env.around( diff --git a/packages/cli/src/deno.ts b/packages/cli/src/deno.ts index 8fbc412c7..af475f134 100644 --- a/packages/cli/src/deno.ts +++ b/packages/cli/src/deno.ts @@ -13,6 +13,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileDataUri } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; +import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; import { useDenoRepl } from "./deno-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; @@ -70,6 +71,7 @@ if (isCredentialHelperMode(process.argv.slice(2))) { await main(() => runCredentialHelper(process.argv.slice(2))); } else { await main(function* (args) { + yield* useDenoComponentModuleLoader(); // The base providers for this host. `at: "min"` puts them beneath ordinary // middleware, so middleware installed later can wrap either one. yield* API.Env.around( diff --git a/packages/cli/src/node.ts b/packages/cli/src/node.ts index 2c8bcbd18..bcec09b36 100755 --- a/packages/cli/src/node.ts +++ b/packages/cli/src/node.ts @@ -17,6 +17,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileTempFile } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; +import { useNodeComponentModuleLoader } from "@executablemd/core/component-modules/node"; import { useNodeRepl } from "./node-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; @@ -43,6 +44,7 @@ const UPGRADE: UpgradeAssembly = { }; await main(function* (args) { + yield* useNodeComponentModuleLoader(); // The base providers for this host. `at: "min"` puts them beneath ordinary // middleware, so middleware installed later can wrap either one. yield* API.Env.around( diff --git a/packages/cli/src/testing-host.ts b/packages/cli/src/testing-host.ts index b0d768f05..fff79a8e0 100644 --- a/packages/cli/src/testing-host.ts +++ b/packages/cli/src/testing-host.ts @@ -61,6 +61,7 @@ import { installDocumentComponents } from "./cli.ts"; import { installPlugins } from "./plugin-host.ts"; import { nestedRunProfile } from "./run-profile.ts"; import type { Plugin } from "@executablemd/core/api"; +import { ComponentModuleLoader } from "@executablemd/core/api"; import { useOrdinaryGeneratedSelection } from "./evaluation-profile.ts"; import type { HostServiceInstaller } from "./cli.ts"; @@ -87,6 +88,7 @@ export interface ChildPlanDeclaration { /** What the entrypoint already decided, and a child must not decide again. */ export interface TestingHostSettings { + readonly componentModuleLoader?: ComponentModuleLoader; /** The component search path this run resolves names through. */ readonly includes: string[]; /** @@ -270,6 +272,7 @@ function* runProfileChild( settings: TestingHostSettings, ): Operation { const request = invocation.request; + yield* ComponentModuleLoader.set(settings.componentModuleLoader); if (request.host !== "run") { throw new Error(`the ${request.host} host profile is not available on this entrypoint`); } diff --git a/packages/cli/tests/testing-execution-host-native.test.ts b/packages/cli/tests/testing-execution-host-native.test.ts index b36626352..0189ace23 100644 --- a/packages/cli/tests/testing-execution-host-native.test.ts +++ b/packages/cli/tests/testing-execution-host-native.test.ts @@ -23,6 +23,8 @@ import { Execution } from "@executablemd/core/api"; import type { Plugin, PluginInstallation } from "@executablemd/core/api"; import { cwd } from "@executablemd/runtime"; import { cliBase } from "@executablemd/test-support/launch"; +import { runCli, runShell, shellQuote } from "@executablemd/test-support/launch"; +import { writeTextFile } from "@effectionx/fs"; import { useTempDirectory } from "@executablemd/test-support/temp"; import type { HelperAssembly } from "@executablemd/git/credential-helper"; import { planComponentDeclaration } from "../src/plan-component.ts"; @@ -48,6 +50,50 @@ import { denoRunRepositories } from "../src/deno-repositories.ts"; * configured. */ describe("C1 — the native host a run child initializes", () => { + it("L4: source entrypoints install the loader and carry it into detached run children", function* () { + const project = yield* useTempDirectory("xmd-module-host-"); + const repository = fileURLToPath(new URL("../../../", import.meta.url)); + yield* writeTextFile( + join(project, "helper.ts"), + 'export const value: string = "host-typed-component";', + ); + yield* writeTextFile( + join(project, "Typed.ts"), + 'import { value } from "./helper.ts";\nexport default function*() { const typed: string = value; return typed; }', + ); + yield* writeTextFile( + join(project, "README.test.md"), + [ + '', + '', + '', + '"} as="child">', + '', + '', + "", + "", + ].join("\n"), + ); + const args = ["test", "README.test.md", "--include", project]; + console.log(JSON.stringify({ argv: [...cliBase(), ...args], cwd: project })); + const deno = yield* runCli(args, { cwd: project }).join(); + expect({ code: deno.code, stderr: deno.stderr }).toEqual({ code: 0, stderr: "" }); + for (const argv of [ + [ + "tsx", + "--tsconfig", + join(repository, "tsconfig.node.json"), + join(repository, "packages/cli/src/node.ts"), + ...args, + ], + ["bun", join(repository, "packages/cli/src/bun.ts"), ...args], + ]) { + console.log(JSON.stringify({ argv, cwd: project })); + const result = yield* runShell(argv.map(shellQuote).join(" "), { cwd: project }).join(); + expect({ argv, code: result.code, stderr: result.stderr }).toMatchObject({ argv, code: 0 }); + } + }); + /** A bare remote a child may clone without reaching a network. */ function* useBareRemote(home: string): Operation { const parent = yield* until(realpath(yield* useTempDirectory("xmd-c1-remote-"))); diff --git a/packages/core/api.ts b/packages/core/api.ts index 79695c42a..165ece0e1 100644 --- a/packages/core/api.ts +++ b/packages/core/api.ts @@ -33,6 +33,7 @@ */ export { Plugin } from "./src/plugin.ts"; +export { ComponentModuleLoader } from "./src/component-module-loader.ts"; export type { PluginInstallation, PluginInstallRequest } from "./src/plugin.ts"; export { diff --git a/packages/core/component-modules/bun.ts b/packages/core/component-modules/bun.ts new file mode 100644 index 000000000..6aab123fd --- /dev/null +++ b/packages/core/component-modules/bun.ts @@ -0,0 +1,46 @@ +import { allSettled, ensure, until } from "effection"; +import type { Operation } from "effection"; +import { dirname, isAbsolute } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { ComponentModuleLoader } from "../src/component-module-loader.ts"; +import { componentSource } from "./transform.ts"; + +function* load(source: string, origin: string): Operation { + const bun: unknown = Reflect.get(globalThis, "Bun"); + if ( + typeof bun !== "object" || + bun === null || + !("resolveSync" in bun) || + typeof bun.resolveSync !== "function" + ) { + throw new Error("The Bun component module loader requires Bun.resolveSync"); + } + const resolveSync = bun.resolveSync.bind(bun); + const emitted = componentSource(source, origin, (specifier) => { + const resolved: unknown = resolveSync(specifier, dirname(fileURLToPath(origin))); + if (typeof resolved !== "string") { + throw new Error("Bun.resolveSync returned no module path"); + } + return isAbsolute(resolved) ? pathToFileURL(resolved).href : resolved; + }); + let url: string | undefined; + const pending: Operation[] = []; + yield* ensure(function* () { + try { + yield* allSettled(pending); + } finally { + if (url !== undefined) { + URL.revokeObjectURL(url); + } + } + }); + url = URL.createObjectURL(new Blob([emitted], { type: "text/javascript" })); + const imported = until(import(url)); + pending.push(imported); + return yield* imported; +} + +/** Install the Bun host's in-memory TypeScript module loader. */ +export function* useBunComponentModuleLoader(): Operation { + yield* ComponentModuleLoader.set({ load }); +} diff --git a/packages/core/component-modules/deno.ts b/packages/core/component-modules/deno.ts new file mode 100644 index 000000000..bc93793a0 --- /dev/null +++ b/packages/core/component-modules/deno.ts @@ -0,0 +1,30 @@ +import * as moduleApi from "node:module"; +import type { Operation } from "effection"; +import { ComponentModuleLoader } from "../src/component-module-loader.ts"; +import { loadHookModule } from "./hooks.ts"; + +/** Install the Deno and compiled host's in-memory TypeScript module loader. */ +export function* useDenoComponentModuleLoader(): Operation { + yield* ComponentModuleLoader.set({ + load: (source, origin) => + loadHookModule(source, origin, (options) => + moduleApi.registerHooks({ + ...options, + load(url, context, next) { + const delegate: typeof next = (url, context) => { + const loaded = next(url, context); + // Deno's delegated CommonJS JSON load omits its native format hint. + return loaded.format == null && + url.startsWith("file:") && + new URL(url).pathname.endsWith(".json") + ? { ...loaded, format: "json" } + : loaded; + }; + return options.load === undefined + ? delegate(url, context) + : options.load(url, context, delegate); + }, + }), + ), + }); +} diff --git a/packages/core/component-modules/hooks.ts b/packages/core/component-modules/hooks.ts new file mode 100644 index 000000000..1225ec036 --- /dev/null +++ b/packages/core/component-modules/hooks.ts @@ -0,0 +1,50 @@ +import { allSettled, ensure, until } from "effection"; +import type { Operation } from "effection"; +import type { RegisterHooksOptions, ModuleHooks } from "node:module"; +import { componentSource } from "./transform.ts"; + +/** Keep one host's supplied-source hooks until the consuming invocation ends. */ +export function* loadHookModule( + source: string, + origin: string, + register: (hooks: RegisterHooksOptions) => ModuleHooks, +): Operation { + const digest = new Uint8Array( + yield* until(crypto.subtle.digest("SHA-256", new TextEncoder().encode(source))), + ); + const url = new URL(origin); + url.searchParams.set( + "xmd-component-source", + Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""), + ); + const selected = url.href; + const emitted = componentSource(source, origin); + let hooks: ModuleHooks | undefined; + const pending: Operation[] = []; + yield* ensure(function* () { + try { + yield* allSettled(pending); + } finally { + hooks?.deregister(); + } + }); + hooks = register({ + resolve(specifier, context, next) { + if (specifier === selected) { + return { url: selected, format: "module", shortCircuit: true }; + } + return next( + specifier, + context.parentURL === selected ? { ...context, parentURL: origin } : context, + ); + }, + load(url, context, next) { + return url === selected + ? { source: emitted, format: "module", shortCircuit: true } + : next(url, context); + }, + }); + const imported = until(import(selected)); + pending.push(imported); + return yield* imported; +} diff --git a/packages/core/component-modules/node.ts b/packages/core/component-modules/node.ts new file mode 100644 index 000000000..22f6999a0 --- /dev/null +++ b/packages/core/component-modules/node.ts @@ -0,0 +1,11 @@ +import * as moduleApi from "node:module"; +import type { Operation } from "effection"; +import { ComponentModuleLoader } from "../src/component-module-loader.ts"; +import { loadHookModule } from "./hooks.ts"; + +/** Install the Node host's in-memory TypeScript module loader. */ +export function* useNodeComponentModuleLoader(): Operation { + yield* ComponentModuleLoader.set({ + load: (source, origin) => loadHookModule(source, origin, moduleApi.registerHooks), + }); +} diff --git a/packages/core/component-modules/transform.ts b/packages/core/component-modules/transform.ts new file mode 100644 index 000000000..204f6ddee --- /dev/null +++ b/packages/core/component-modules/transform.ts @@ -0,0 +1,164 @@ +import Babel from "@babel/standalone"; +import { basename, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +type Plugin = Extract< + NonNullable[1]["plugins"]>[number], + (...args: never[]) => unknown +>; + +/** Emit an ordinary ES module without reading compiler configuration. */ +export function componentSource( + source: string, + origin: string, + resolve?: (specifier: string) => string, +): string { + const typed = Babel.transform(source, { + filename: fileURLToPath(origin), + sourceFileName: origin, + sourceType: "module", + envName: "production", + presets: [["typescript", { onlyRemoveTypeImports: true }]], + sourceMaps: true, + }); + if (typed?.code === undefined || typed.code === null) { + throw new Error("TypeScript component transformation produced no module"); + } + const plugin: Plugin = (api) => { + const { types: t, template } = api; + let metadata: Babel.packages.types.Identifier; + let resolver: Babel.packages.types.Identifier; + let specifier: Babel.packages.types.Identifier; + return { + visitor: { + Program: { + enter(path) { + metadata = path.scope.generateUidIdentifier("componentMeta"); + resolver = path.scope.generateUidIdentifier("componentImport"); + specifier = path.scope.generateUidIdentifier("componentSpecifier"); + }, + exit(path) { + if (resolve === undefined) { + path.node.body.unshift( + t.expressionStatement( + t.assignmentExpression( + "=", + t.memberExpression( + t.metaProperty(t.identifier("import"), t.identifier("meta")), + t.identifier("url"), + ), + t.stringLiteral(origin), + ), + ), + ); + return; + } + const nativeResolve = path.scope.generateUidIdentifier("resolveSync"); + const toURL = path.scope.generateUidIdentifier("pathToFileURL"); + const absolute = path.scope.generateUidIdentifier("isAbsolute"); + const require = path.scope.generateUidIdentifier("createRequire"); + const filename = fileURLToPath(origin); + const injected = template.program.ast(` + import { resolveSync as ${nativeResolve.name} } from "bun"; + import { pathToFileURL as ${toURL.name} } from "node:url"; + import { isAbsolute as ${absolute.name} } from "node:path"; + import { createRequire as ${require.name} } from "node:module"; + const ${resolver.name} = (input) => { + const resolved = ${nativeResolve.name}(\`\${input}\`, ${JSON.stringify(dirname(filename))}); + return ${absolute.name}(resolved) ? ${toURL.name}(resolved).href : resolved; + }; + const ${specifier.name} = (input) => ({ + [Symbol.toPrimitive]: () => ${resolver.name}(input) + }); + const ${metadata.name} = Object.assign(Object.create(null), import.meta, { + url: ${JSON.stringify(origin)}, filename: ${JSON.stringify(filename)}, + dirname: ${JSON.stringify(dirname(filename))}, path: ${JSON.stringify(filename)}, + dir: ${JSON.stringify(dirname(filename))}, file: ${JSON.stringify(basename(filename))}, + main: false, require: ${require.name}(${JSON.stringify(origin)}), + resolve: (input, parent) => parent === undefined ? ${resolver.name}(input) : + (() => { const value = ${nativeResolve.name}(input, parent); return ${absolute.name}(value) ? ${toURL.name}(value).href : value; })(), + resolveSync: (input, parent = ${JSON.stringify(dirname(filename))}) => ${nativeResolve.name}(input, parent) + }); + `); + path.node.body.unshift(...injected.body); + }, + }, + ImportDeclaration(path) { + if (resolve !== undefined) { + path.node.source.value = resolve(path.node.source.value); + } + }, + ExportNamedDeclaration(path) { + if (resolve !== undefined && path.node.source != null) { + path.node.source.value = resolve(path.node.source.value); + } + }, + ExportAllDeclaration(path) { + if (resolve !== undefined) { + path.node.source.value = resolve(path.node.source.value); + } + }, + ImportExpression(path) { + if (resolve !== undefined) { + // Native import coerces the specifier after evaluating its options, + // and turns coercion/resolution failures into promise rejections. + path.node.source = t.callExpression(t.cloneNode(specifier), [path.node.source]); + } + }, + CallExpression(path) { + if (resolve !== undefined && t.isImport(path.node.callee)) { + const argument = path.node.arguments[0]; + if (t.isExpression(argument)) { + path.node.arguments[0] = t.callExpression(t.cloneNode(specifier), [argument]); + } + } + }, + MetaProperty(path) { + if (resolve !== undefined && path.node.meta.name === "import") { + path.replaceWith(t.cloneNode(metadata)); + } + }, + ReferencedIdentifier(path) { + if ( + resolve !== undefined && + path.node.name === "require" && + !path.scope.hasBinding("require") + ) { + path.replaceWith(t.memberExpression(t.cloneNode(metadata), t.identifier("require"))); + } + }, + }, + }; + }; + const transformed = Babel.transform(typed.code, { + filename: fileURLToPath(origin), + sourceFileName: origin, + sourceType: "module", + envName: "production", + sourceMaps: true, + ...(typed.map == null + ? {} + : { + inputSourceMap: { + file: fileURLToPath(origin), + version: typed.map.version, + names: [...typed.map.names], + sources: typed.map.sources.map((value) => value ?? ""), + mappings: typed.map.mappings, + ...(typed.map.sourcesContent == null + ? {} + : { sourcesContent: typed.map.sourcesContent.map((value) => value ?? "") }), + }, + }), + plugins: [plugin], + }); + if (transformed?.code === undefined || transformed.code === null) { + throw new Error("TypeScript component transformation produced no module"); + } + const map = btoa( + Array.from(new TextEncoder().encode(JSON.stringify(transformed.map)), (byte) => + String.fromCharCode(byte), + ).join(""), + ); + return `${transformed.code}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${map}`; +} diff --git a/packages/core/src/component-module-loader.ts b/packages/core/src/component-module-loader.ts new file mode 100644 index 000000000..afbb9bc50 --- /dev/null +++ b/packages/core/src/component-module-loader.ts @@ -0,0 +1,12 @@ +import { createContext } from "effection"; +import type { Context, Operation } from "effection"; + +export interface ComponentModuleLoader { + /** Load selected direct TypeScript bytes at their absolute logical file URL. */ + load(source: string, origin: string): Operation; +} + +/** Select the host loader without granting permissions or loading a module. */ +export const ComponentModuleLoader: Context = createContext< + ComponentModuleLoader | undefined +>("@executablemd/core/ComponentModuleLoader", undefined); diff --git a/packages/core/src/execute.ts b/packages/core/src/execute.ts index 347987dbb..73ac342d4 100644 --- a/packages/core/src/execute.ts +++ b/packages/core/src/execute.ts @@ -28,6 +28,9 @@ import { import { API, readTextFile, cwd, timeoutExec } from "@executablemd/runtime"; import type { ProcessOutcome } from "@executablemd/runtime"; import { cwd as processCwd } from "@effectionx/fs"; +import { useComponentModules } from "./typescript-module.ts"; +import type { LoadComponentModule } from "./typescript-module.ts"; +import { pathToFileURL } from "node:url"; import type { Workflow, Json } from "@executablemd/durable-streams"; import { createReplayStream } from "./replay-stream.ts"; import { ExecutionProtocolError, issueExecution } from "./execution-request.ts"; @@ -484,6 +487,7 @@ function targetFailureRecord(failure: DocumentTargetFailure): TargetFailureRecor * the property this shape exists to make true rather than to promise. */ interface ImportInputs { + readonly loadModule: LoadComponentModule; readonly searchPaths: string[]; readonly registry: ComponentRegistry; readonly bundle: WorkflowComponentCatalog | undefined; @@ -712,7 +716,7 @@ function* materializeImport( // content can resolve, or leave a selected path unloadable. const currentDir = yield* processCwd(); const absolutePath = path.startsWith("/") ? path : `${currentDir}/${path}`; - const mod = yield* until(import(`file://${absolutePath}`)); + const mod = yield* inputs.loadModule(content, pathToFileURL(absolutePath).href); if (typeof mod !== "object" || mod === null) { throw new Error(`Function component "${name}" at ${path} did not load a module`); } @@ -724,10 +728,16 @@ function* materializeImport( // authenticated against the invocation this engine minted. const declaration = parseFormDeclaration("form" in mod ? mod.form : undefined); const defaultExport = "default" in mod ? mod.default : undefined; - if (declaration === undefined && !isFunctionComponent(defaultExport)) { - throw new Error( - `Function component "${name}" at ${path} must have a default export that is a generator function`, - ); + let fn: FunctionComponent; + if (declaration !== undefined) { + fn = formDispatcher(declaration); + } else { + if (!isFunctionComponent(defaultExport)) { + throw new Error( + `Function component "${name}" at ${path} must have a default export that is a generator function`, + ); + } + fn = defaultExport; } const propsExport = "props" in mod ? mod.props : undefined; @@ -756,7 +766,7 @@ function* materializeImport( // component accepts and what it says it accepts come from one value. ...(forms === undefined ? {} : { forms }), ...(offered === undefined ? {} : { generated: offered.value }), - fn: declaration === undefined ? defaultExport : formDispatcher(declaration), + fn, }; if ("returns" in mod && mod.returns !== undefined) { @@ -2602,6 +2612,7 @@ export interface HostExecution extends DocumentExecution { * under and what a document imports through are the same decisions. */ interface ComponentAssembly { + readonly loadModule: LoadComponentModule; readonly startingRegistry: ComponentRegistry; readonly bundle: WorkflowComponentCatalog | undefined; readonly catalog: ExecutionDeclarationCatalog; @@ -2659,6 +2670,7 @@ function* assembleComponents( documentation: readonly DocumentationContribution[], canonicalImports: CanonicalImports, lifetime: EvaluationLifetime, + loadModule: LoadComponentModule, ): Operation { // The paths *this capture* resolves against, copied: the selection below is // resolved against the environment the host assembled, and a later edit to @@ -2791,6 +2803,7 @@ function* assembleComponents( declared: installedComponents, catalog, guarded: identity.protected, + loadModule, }), lifetime, identity.componentRouting.project, @@ -2798,6 +2811,7 @@ function* assembleComponents( return { startingRegistry, + loadModule, bundle, catalog, installedComponents, @@ -2991,6 +3005,7 @@ function* executeDocument( declared: installedComponents, catalog, guarded: identity.protected, + loadModule: assembly.loadModule, }, admitted, ); @@ -4162,6 +4177,7 @@ function* invoke( documentation, canonicalImports, lifetime, + yield* useComponentModules(), ); // Whatever a handler returns is not an execution, so it is not read. diff --git a/packages/core/src/typescript-module.ts b/packages/core/src/typescript-module.ts new file mode 100644 index 000000000..d321cd0d6 --- /dev/null +++ b/packages/core/src/typescript-module.ts @@ -0,0 +1,47 @@ +import { ensure, Err, Ok, scoped, suspend, useScope, withResolvers } from "effection"; +import type { Operation, Result, Task } from "effection"; +import { ComponentModuleLoader } from "./component-module-loader.ts"; + +export type LoadComponentModule = (source: string, origin: string) => Operation; + +/** Keep native module resources on the canonical invocation, beyond ephemeral imports. */ +export function* useComponentModules(): Operation { + const selected = yield* ComponentModuleLoader.get(); + const load = selected?.load.bind(selected); + const owner = yield* useScope(); + + return (source, origin) => + scoped(function* () { + if (load === undefined) { + throw new Error( + "TypeScript component module loader is not installed — bind ComponentModuleLoader for this host before loading TypeScript components.", + ); + } + const ready = withResolvers>(); + let child: Task | undefined; + let transferred = false; + yield* ensure(function* () { + if (!transferred && child !== undefined) { + yield* child.halt(); + } + }); + child = owner.run(function* () { + let value: unknown; + try { + value = yield* load(source, origin); + } catch (error) { + ready.resolve(Err(error)); + return; + } + ready.resolve(Ok(value)); + yield* suspend(); + }); + const result = yield* ready.operation; + if (!result.ok) { + yield* child; + throw result.error; + } + transferred = true; + return result.value; + }); +} diff --git a/packages/core/tests/component-module-loader.test.ts b/packages/core/tests/component-module-loader.test.ts new file mode 100644 index 000000000..c3f47c734 --- /dev/null +++ b/packages/core/tests/component-module-loader.test.ts @@ -0,0 +1,274 @@ +import { describe, it } from "@executablemd/test-support/bdd"; +import { expect } from "@executablemd/test-support/expect"; +import { useTempDirectory } from "@executablemd/test-support/temp"; +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { cliRuntime } from "@executablemd/test-support/launch"; +import { readTextFile, readdir, writeTextFile } from "@effectionx/fs"; +import { exec } from "@effectionx/process"; +import { createContext, ensure, scoped, spawn, suspend, until, withResolvers } from "effection"; +import type { Operation } from "effection"; +import * as moduleApi from "node:module"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { InMemoryStream } from "@executablemd/durable-streams"; +import { collect, Component, execute, registerComponents, useNormalizedOutput } from "../mod.ts"; +import { ComponentModuleLoader, Execution } from "../api.ts"; +import { useTempFileCompiler } from "../src/temp-file-compiler.ts"; +import { loadHookModule } from "../component-modules/hooks.ts"; + +const ABSENT = + "TypeScript component module loader is not installed — bind ComponentModuleLoader for this host before loading TypeScript components."; +const SOURCE = + 'export const generated = { "self-closing": { identity: "https://executable.md/tests/module/typed", effect: "read" } };\nexport default function*() { const answer: string = "typed-module"; return answer; }'; +const ROOT = fileURLToPath(new URL("../../../", import.meta.url)); + +function render(directory: string, source = ""): Operation { + return scoped(function* () { + yield* useNormalizedOutput(); + return yield* collect( + yield* execute({ + source, + path: "", + includes: [directory], + stream: new InMemoryStream(), + }), + ); + }); +} + +describe("ComponentModuleLoader", () => { + it("L1: requires explicit installation independently of eval, Markdown and registrations", function* () { + const directory = yield* useTempDirectory("component-loader-install-"); + const entered = `loader-absent-${directory}`; + yield* writeTextFile( + join(directory, "Typed.ts"), + `Reflect.set(globalThis, ${JSON.stringify(entered)}, true);\n${SOURCE}`, + ); + yield* ComponentModuleLoader.set(undefined); + for (const evalCompiler of [false, true]) { + yield* scoped(function* () { + if (evalCompiler) { + yield* useTempFileCompiler(); + } + let diagnostic = ""; + try { + yield* render(directory); + } catch (error) { + diagnostic = error instanceof Error ? error.message : String(error); + } + expect(diagnostic).toContain(ABSENT); + expect(Reflect.has(globalThis, entered)).toBe(false); + expect((yield* readdir(directory)).sort()).toEqual(["Typed.ts"]); + }); + } + expect(yield* render(directory, "Markdown alone")).toContain("Markdown alone"); + yield* registerComponents([ + { + name: "Registered", + origin: "test://registered", + props: { type: "object" }, + fn: function* () { + return "registered"; + }, + }, + ]); + expect(yield* render(directory, "")).toContain("registered"); + yield* ensure(function* () { + Reflect.deleteProperty(globalThis, entered); + }); + yield* useComponentModuleLoader(); + expect(yield* render(directory)).toContain("typed-module"); + expect(Reflect.get(globalThis, entered)).toBe(true); + }); + + it("L2: captures one bound selection for profile and durable loading across later rebinding", function* () { + yield* useComponentModuleLoader(); + const native = yield* ComponentModuleLoader.expect(); + if (native === undefined) { + throw new Error("fixture installer selected no loader"); + } + const directory = yield* useTempDirectory("component-loader-capture-"); + const origin = pathToFileURL(join(directory, "Typed.ts")).href; + yield* writeTextFile(join(directory, "Typed.ts"), SOURCE); + const loadNative = native.load.bind(native); + const calls: Array<{ provider: string; source: string; origin: string }> = []; + function provider(label: string): ComponentModuleLoader { + return { + *load(source, origin) { + expect(this).toBe(selected.get(label)); + calls.push({ provider: label, source, origin }); + return yield* loadNative(source, origin); + }, + }; + } + const selected = new Map(); + selected.set("A", provider("A")); + selected.set("B", provider("B")); + const A = selected.get("A"); + const B = selected.get("B"); + const copy = createContext( + "@executablemd/core/ComponentModuleLoader", + undefined, + ); + yield* copy.set(A); + expect(yield* ComponentModuleLoader.get()).toBe(A); + yield* scoped(function* () { + yield* Component.around({ generated: () => ({ Typed: ["self-closing"] }) }); + yield* Execution.around({ + *execute([request], next) { + yield* ComponentModuleLoader.set(B); + yield* next(request); + }, + }); + expect(yield* render(directory, '\n')).toContain( + "typed-module", + ); + }); + expect(calls.length).toBeGreaterThanOrEqual(2); + expect( + calls.every( + (call) => call.provider === "A" && call.source === SOURCE && call.origin === origin, + ), + ).toBe(true); + calls.length = 0; + yield* copy.set(B); + expect(yield* render(directory)).toContain("typed-module"); + expect(calls).toEqual([{ provider: "B", source: SOURCE, origin }]); + }); + + it("L3: joins native loading before releasing its owned hook or Blob", function* () { + yield* useComponentModuleLoader(); + const native = yield* ComponentModuleLoader.get(); + if (native === undefined) { + throw new Error("fixture installer selected no loader"); + } + const directory = yield* useTempDirectory("component-loader-lifetime-"); + const origin = pathToFileURL(join(directory, "Lifetime.ts")).href; + yield* writeTextFile(join(directory, "late.mjs"), 'export const value = "late-native";'); + const entered = withResolvers(); + const stopped = withResolvers(); + const gate = `component-loader-native-${directory}`; + let release: () => void = () => { + throw new Error("native module has not entered"); + }; + let removed = 0; + let installed = 0; + const source = `await new Promise(resolve => Reflect.get(globalThis, ${JSON.stringify(gate)})(resolve));\nexport const later = () => import("./late.mjs");`; + Reflect.set(globalThis, gate, (resolve: () => void) => { + release = resolve; + entered.resolve(); + }); + yield* ensure(function* () { + Reflect.deleteProperty(globalThis, gate); + }); + let loader = native.load.bind(native); + if (cliRuntime() === "bun") { + const create = URL.createObjectURL; + const revoke = URL.revokeObjectURL; + yield* ensure(function* () { + URL.createObjectURL = create; + URL.revokeObjectURL = revoke; + }); + URL.createObjectURL = (blob) => { + installed++; + return create(blob); + }; + URL.revokeObjectURL = (url) => { + removed++; + revoke(url); + }; + } else { + loader = (source, origin) => + loadHookModule(source, origin, (options) => { + const handle = moduleApi.registerHooks(options); + installed++; + const deregister = handle.deregister.bind(handle); + return new Proxy(handle, { + get(target, property, receiver) { + return property === "deregister" + ? () => { + removed++; + deregister(); + } + : Reflect.get(target, property, receiver); + }, + }); + }); + } + const owner = yield* spawn(function* () { + yield* loader(source, origin); + yield* suspend(); + }); + yield* entered.operation; + const halt = yield* spawn(function* () { + yield* owner.halt(); + stopped.resolve(); + }); + expect(installed).toBe(1); + expect(removed).toBe(0); + release(); + yield* stopped.operation; + yield* halt; + expect(removed).toBe(1); + const result = yield* scoped(function* () { + const module = yield* loader('export const later = () => import("./late.mjs");', origin); + if ( + typeof module !== "object" || + module === null || + !("later" in module) || + typeof module.later !== "function" + ) { + throw new Error("native module lost its export"); + } + expect(removed).toBe(1); + return yield* until(module.later()); + }); + expect(result).toMatchObject({ value: "late-native" }); + expect(installed).toBe(2); + expect(removed).toBe(2); + expect(yield* until(import(pathToFileURL(join(directory, "late.mjs")).href))).toMatchObject({ + value: "late-native", + }); + }); + + it("P1: loads under the preserved read-only envelope and retains authored write denial", function* () { + const directory = yield* useTempDirectory("component-loader-readonly-"); + const direct = join(directory, "ReadOnly.ts"); + yield* writeTextFile( + direct, + 'export default function*() { const value: string = "ordinary-read-only-component"; return value; }', + ); + const before = yield* readTextFile(direct); + const fixture = join(ROOT, "packages/core/tests/fixtures/component-module-readonly.ts"); + for (const mode of ["read", "write"]) { + const result = yield* exec("deno", { + arguments: [ + "run", + "--config", + join(ROOT, "deno.json"), + "--node-modules-dir=manual", + "--allow-read", + "--allow-env", + "--allow-import", + "--deny-write", + "--no-prompt", + "--cached-only", + "--frozen", + fixture, + directory, + mode, + ], + }).join(); + expect({ mode, code: result.code, stderr: result.stderr }).toEqual({ + mode, + code: 0, + stderr: "", + }); + expect(result.stdout.trim()).toBe( + mode === "read" ? "ordinary-read-only-component" : "authored-write-denied", + ); + } + expect(yield* readTextFile(direct)).toBe(before); + expect((yield* readdir(directory)).sort()).toEqual(["ReadOnly.ts"]); + }); +}); diff --git a/packages/core/tests/component-registration.test.ts b/packages/core/tests/component-registration.test.ts index 42694c665..8c508dea5 100644 --- a/packages/core/tests/component-registration.test.ts +++ b/packages/core/tests/component-registration.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; /** * Tier CR — component registration and resolution (spec §5.3). * @@ -701,6 +702,7 @@ describe("Tier CR — what a document gets", () => { // used verbatim and never consults a working directory at all, which is why // the fixture lives under the process's own directory. it("CR24b: a relative repository component loads inside a rebound cwd", function* () { + yield* useComponentModuleLoader(); const dir = yield* useLocalFixture(); yield* writeTextFile(join(dir, "doc.md"), "\n\n\n"); yield* writeTextFile( diff --git a/packages/core/tests/component-returns.test.ts b/packages/core/tests/component-returns.test.ts index 38776379b..b00ccb5ca 100644 --- a/packages/core/tests/component-returns.test.ts +++ b/packages/core/tests/component-returns.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; /** * Tier RV — component and root return values (spec §6.10, §5.4). * @@ -50,6 +51,7 @@ function* runExecution(options: { commands: string[]; }): Operation { const { commands } = options; + yield* useComponentModuleLoader(); yield* API.Process.around({ // deno-lint-ignore require-yield *exec([execOptions], _next) { @@ -630,6 +632,7 @@ describe("Tier RV — component return values", () => { delete globals.__rv12torn; }); + yield* useComponentModuleLoader(); yield* scoped(function* () { const dir = path.join(os.tmpdir(), `returns-halt-${randomUUID()}`); yield* ensureDir(dir); diff --git a/packages/core/tests/contextual-cwd.test.ts b/packages/core/tests/contextual-cwd.test.ts index ccb86774e..d205fef2b 100644 --- a/packages/core/tests/contextual-cwd.test.ts +++ b/packages/core/tests/contextual-cwd.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; /** * Tier CW — contextual working directory (spec §1.2). * @@ -105,6 +106,7 @@ function writeDocument(fixture: Fixture, source: string): Operation { function run(fixture: Fixture): Operation { return scoped(function* () { let displayed = ""; + yield* useComponentModuleLoader(); const decoder = new TextDecoder(); yield* Stdio.around({ *stdout([bytes]) { diff --git a/packages/core/tests/fixtures/component-module-readonly.ts b/packages/core/tests/fixtures/component-module-readonly.ts new file mode 100644 index 000000000..5cf8b561e --- /dev/null +++ b/packages/core/tests/fixtures/component-module-readonly.ts @@ -0,0 +1,38 @@ +import { main } from "effection"; +import { argv } from "node:process"; +import { join } from "node:path"; +import { writeTextFile } from "@effectionx/fs"; +import { collect, execute, useNormalizedOutput } from "@executablemd/core"; +import { InMemoryStream } from "@executablemd/durable-streams"; +import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; + +main(function* () { + const directory = argv[2]; + if (directory === undefined) { + throw new Error("missing fixture directory"); + } + yield* useDenoComponentModuleLoader(); + if (argv[3] === "write") { + try { + yield* writeTextFile(join(directory, "denied.txt"), "denied"); + } catch (error) { + if (error instanceof Error && error.name === "NotCapable") { + console.log("authored-write-denied"); + return; + } + throw error; + } + throw new Error("the loader granted an authored write"); + } + yield* useNormalizedOutput(); + console.log( + yield* collect( + yield* execute({ + source: "", + path: "", + includes: [directory], + stream: new InMemoryStream(), + }), + ), + ); +}); diff --git a/packages/core/tests/fixtures/typescript-materialization.ts b/packages/core/tests/fixtures/typescript-materialization.ts new file mode 100644 index 000000000..89e5c9454 --- /dev/null +++ b/packages/core/tests/fixtures/typescript-materialization.ts @@ -0,0 +1,53 @@ +import { readTextFile, writeTextFile } from "@effectionx/fs"; +import { main } from "effection"; +import { + parseDurableEvent, + serializeDurableEvent, + InMemoryStream, +} from "@executablemd/durable-streams"; +import { collect, execute, registerComponents, useNormalizedOutput } from "@executablemd/core"; +import { join } from "node:path"; +import { argv } from "node:process"; +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; + +main(function* () { + yield* useComponentModuleLoader(); + const directory = argv[2]; + if (directory === undefined) { + throw new Error("missing fixture project"); + } + const events = (yield* readTextFile(join(directory, "journal.jsonl"))) + .trim() + .split("\n") + .map((line) => { + const parsed = parseDurableEvent(line); + if (!parsed.ok) { + throw parsed.error; + } + return parsed.value; + }); + const stream = new InMemoryStream(events); + yield* useNormalizedOutput(); + yield* registerComponents([ + { + name: "Gate", + props: { type: "object", properties: {}, additionalProperties: false }, + fn: function* () { + return "continued"; + }, + origin: "materialization-fixture", + }, + ]); + const result = yield* collect( + yield* execute({ + path: join(directory, "doc.md"), + includes: [join(directory, "components")], + stream, + }), + ); + yield* writeTextFile(join(directory, "result.json"), JSON.stringify(result)); + yield* writeTextFile( + join(directory, "continued.jsonl"), + stream.snapshot().map(serializeDurableEvent).join(""), + ); +}); diff --git a/packages/core/tests/function-components.test.ts b/packages/core/tests/function-components.test.ts index b06b62e46..1dd21fbca 100644 --- a/packages/core/tests/function-components.test.ts +++ b/packages/core/tests/function-components.test.ts @@ -16,6 +16,7 @@ import type { Json } from "@executablemd/durable-streams"; import { useTempFileCompiler } from "../src/temp-file-compiler.ts"; import { ensureDir, exists, readTextFile, writeTextFile } from "@effectionx/fs"; import { useTempDirectory } from "@executablemd/test-support/temp"; +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; import { until } from "effection"; import { symlink } from "node:fs/promises"; import * as path from "node:path"; @@ -35,6 +36,7 @@ const ROOT = fileURLToPath(new URL("../../../", import.meta.url)); * Teardown unlinks the node_modules symlink rather than following it. */ function* makeTempDir(): Operation { + yield* useComponentModuleLoader(); const dir = yield* useTempDirectory("fc-test-"); yield* writeTextFile(path.join(dir, "package.json"), JSON.stringify({ type: "module" })); yield* until(symlink(path.join(ROOT, "node_modules"), path.join(dir, "node_modules"), "dir")); diff --git a/packages/core/tests/generated-selection.test.ts b/packages/core/tests/generated-selection.test.ts index e788cf515..9c5e230c8 100644 --- a/packages/core/tests/generated-selection.test.ts +++ b/packages/core/tests/generated-selection.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; /** * Tier GS — the component forms a host offers generated XMD (spec §5.3.2). * @@ -75,6 +76,7 @@ function run( ): Operation { return scoped(function* () { const stream = new InMemoryStream(options.events ?? []); + yield* useComponentModuleLoader(); if (options.files !== undefined) { yield* useRecordedFiles(options.files); } diff --git a/packages/core/tests/invocation-identity.test.ts b/packages/core/tests/invocation-identity.test.ts index 3dc35383f..86e894b95 100644 --- a/packages/core/tests/invocation-identity.test.ts +++ b/packages/core/tests/invocation-identity.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; /** * Tier CIV — the identity a trusted host's component names its work after * (specs/executable-mdx-spec.md §5.6). @@ -1435,6 +1436,7 @@ describe("Tier CIV — the authored form on the invocation", () => { }); it("CIV21: a component that imports nothing still reads the canonical form", function* () { + yield* useComponentModuleLoader(); const directory = fileURLToPath(new URL("./fixtures/invocation-identity/", import.meta.url)); // A repository `.ts` component with no imports at all: no helper of its own diff --git a/packages/core/tests/typescript-materialization.test.ts b/packages/core/tests/typescript-materialization.test.ts new file mode 100644 index 000000000..e723b09a0 --- /dev/null +++ b/packages/core/tests/typescript-materialization.test.ts @@ -0,0 +1,258 @@ +import { describe, it } from "@executablemd/test-support/bdd"; +import { expect } from "@executablemd/test-support/expect"; +import { useTempDirectory } from "@executablemd/test-support/temp"; +import { cliBase } from "@executablemd/test-support/launch"; +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { ensureDir, readTextFile, rm, writeTextFile } from "@effectionx/fs"; +import { exec } from "@effectionx/process"; +import { scoped, spawn, suspend, withResolvers } from "effection"; +import type { Operation } from "effection"; +import { realpath, symlink } from "node:fs/promises"; +import { until } from "effection"; +import { join, dirname } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { InMemoryStream, serializeDurableEvent } from "@executablemd/durable-streams"; +import { collect, Component, execute, registerComponents, useNormalizedOutput } from "../mod.ts"; +import type { Json } from "../mod.ts"; + +const ROOT = fileURLToPath(new URL("../../../", import.meta.url)); + +function* project(): Operation { + yield* useComponentModuleLoader(); + const directory = yield* until(realpath(yield* useTempDirectory("typescript-materialization-"))); + yield* writeTextFile(join(directory, "package.json"), JSON.stringify({ type: "module" })); + yield* until(symlink(join(ROOT, "node_modules"), join(directory, "node_modules"), "dir")); + yield* ensureDir(join(directory, "components")); + yield* writeTextFile(join(directory, "components/sibling.txt"), "sibling"); + yield* writeTextFile( + join(directory, "components/late.ts"), + 'export const late: string = "late-native";', + ); + yield* writeTextFile(join(directory, "effects.txt"), ""); + yield* writeTextFile(join(directory, "releases.txt"), ""); + yield* writeTextFile( + join(directory, "components/helper.ts"), + [ + 'import { ensure } from "effection";', + 'import { readTextFile, writeTextFile } from "@effectionx/fs";', + 'export const helper = "relative-helper";', + "export function* append(url: URL, text: string) {", + ' yield* writeTextFile(url, (yield* readTextFile(url)) + text + "\\n");', + "}", + "export function* acquire(origin: string) {", + ' yield* ensure(() => append(new URL("../releases.txt", origin), "released"));', + "}", + ].join("\n"), + ); + return directory; +} + +function source(version: string): string { + return [ + 'import { content, type ComponentExecution, type Json } from "@executablemd/core";', + 'import { createDurableOperation } from "@executablemd/durable-streams";', + 'import { readTextFile } from "@effectionx/fs";', + 'import { until } from "effection";', + 'import { acquire, append, helper } from "./helper.ts";', + 'export const props = { type: "object", properties: { count: { type: "number" } }, required: ["count"], additionalProperties: false };', + 'export const returns = { type: "object", properties: { value: { type: "string" }, origin: { type: "string" }, sibling: { type: "string" }, projected: { type: "string" }, late: { type: "string" }, resolved: { type: "string" } }, required: ["value", "origin", "sibling", "projected", "late", "resolved"], additionalProperties: false };', + "export default function*(props: Record): ComponentExecution {", + " yield* acquire(import.meta.url);", + ' yield createDurableOperation({ type: "materialization-effect", name: "once" }, function* () {', + ' yield* append(new URL("../effects.txt", import.meta.url), "completed");', + ' return "acknowledged";', + " });", + " const projected = yield* content();", + " const order: string[] = [];", + ' const specifier = { toString() { order.push("specifier"); return new URL("./late.ts", import.meta.url).href; } };', + ' const dependency = yield* until(import(specifier, (order.push("options"), {})));', + ' if (order.join(",") !== "options,specifier") { throw new Error("dynamic import changed evaluation order"); }', + ` return { value: ${JSON.stringify(version)} + ":" + props.count + ":" + helper,`, + ' origin: import.meta.url, sibling: yield* readTextFile(new URL("./sibling.txt", import.meta.url)),', + ' projected, late: dependency.late, resolved: import.meta.resolve("./late.ts") };', + "}", + ].join("\n"); +} + +const DOCUMENT = [ + "---", + "returns: { type: object }", + "---", + '', + "", +].join("\n"); + +function* gate(fn: () => Operation): Operation { + yield* registerComponents([ + { + name: "Gate", + props: { type: "object", properties: {}, additionalProperties: false }, + fn, + origin: "materialization-fixture", + }, + ]); +} + +function run(directory: string, stream: InMemoryStream): Operation { + return scoped(function* () { + yield* useNormalizedOutput(); + yield* gate(function* () { + return "continued"; + }); + return yield* collect( + yield* execute({ + path: join(directory, "doc.md"), + includes: [join(directory, "components")], + stream, + }), + ); + }); +} + +function imports(stream: InMemoryStream) { + return stream + .snapshot() + .filter( + (event) => + event.type === "yield" && + event.description.type === "import_component" && + event.description.name === "Direct", + ); +} + +describe("direct TypeScript materialization", () => { + it("F1: the selected bytes replace same-path behavior while preserving the module contract", function* () { + const directory = yield* project(); + const direct = join(directory, "components/Direct.ts"); + yield* writeTextFile(join(directory, "doc.md"), DOCUMENT); + const firstSource = source("first"); + yield* writeTextFile(direct, firstSource); + const firstStream = new InMemoryStream(); + const first = yield* run(directory, firstStream); + expect(first).toEqual({ + value: "first:7:relative-helper", + origin: pathToFileURL(direct).href, + sibling: "sibling", + projected: "continued", + late: "late-native", + resolved: pathToFileURL(join(directory, "components/late.ts")).href, + }); + expect(yield* readTextFile(join(directory, "releases.txt"))).toBe("released\n"); + const secondSource = source("second"); + yield* writeTextFile(direct, secondSource); + const secondStream = new InMemoryStream(); + expect(yield* run(directory, secondStream)).toEqual({ + value: "second:7:relative-helper", + origin: pathToFileURL(direct).href, + sibling: "sibling", + projected: "continued", + late: "late-native", + resolved: pathToFileURL(join(directory, "components/late.ts")).href, + }); + expect(imports(firstStream)).toMatchObject([ + { + result: { status: "ok", value: { kind: "repository", path: direct, content: firstSource } }, + }, + ]); + expect(imports(secondStream)).toMatchObject([ + { + result: { + status: "ok", + value: { kind: "repository", path: direct, content: secondSource }, + }, + }, + ]); + expect(yield* readTextFile(join(directory, "releases.txt"))).toBe("released\nreleased\n"); + + yield* writeTextFile( + join(directory, "components/choice-exports.ts"), + [ + 'import { content } from "@executablemd/core";', + 'export const props = { type: "object", properties: { word: { type: "string" } }, required: ["word"], additionalProperties: false };', + 'export const returns = { type: "string" };', + 'export const generated = { "self-closing": { identity: "https://executable.md/tests/materialization/choice", effect: "read" } };', + 'export const form = { forms: "both",', + ' "self-closing": function*(props) { return "selected:" + props.word; },', + ' paired: function*() { return "paired:" + (yield* content()); }', + "};", + 'export default function*() { throw new Error("default must not dispatch"); }', + ].join("\n"), + ); + yield* writeTextFile( + join(directory, "components/Choice.ts"), + 'export { props, returns, generated, form } from "./choice-exports.ts";\nexport default function*() { throw new Error("default must not dispatch"); }', + ); + yield* scoped(function* () { + yield* useNormalizedOutput(); + yield* Component.around({ generated: () => ({ Choice: ["self-closing"] }) }); + yield* writeTextFile( + join(directory, "form.md"), + '\'} />\n\nbody\n', + ); + const result = yield* collect( + yield* execute({ + path: join(directory, "form.md"), + includes: [dirname(direct)], + stream: new InMemoryStream(), + }), + ); + expect(result).toContain("selected:offered"); + expect(result).toContain("paired:body"); + }); + }); + + it("F2: a fresh process continues retained direct source after its file is removed", function* () { + const directory = yield* project(); + const direct = join(directory, "components/Direct.ts"); + const original = source("retained"); + yield* writeTextFile(direct, original); + yield* writeTextFile(join(directory, "doc.md"), DOCUMENT); + const stream = new InMemoryStream(); + const entered = withResolvers(); + const parent = yield* spawn(function* () { + yield* gate(function* () { + entered.resolve(); + yield* suspend(); + return "unreachable"; + }); + yield* collect( + yield* execute({ path: join(directory, "doc.md"), includes: [dirname(direct)], stream }), + ); + }); + yield* entered.operation; + expect(yield* readTextFile(join(directory, "effects.txt"))).toBe("completed\n"); + expect(imports(stream)).toMatchObject([ + { result: { status: "ok", value: { content: original } } }, + ]); + const prefix = stream.snapshot(); + expect(prefix.some((event) => event.type === "close")).toBe(false); + yield* parent.halt(); + expect(yield* readTextFile(join(directory, "releases.txt"))).toBe("released\n"); + yield* writeTextFile( + join(directory, "journal.jsonl"), + prefix.map(serializeDurableEvent).join(""), + ); + yield* rm(direct); + const [command, ...arguments_] = cliBase(); + arguments_.pop(); + const fixture = fileURLToPath( + new URL("./fixtures/typescript-materialization.ts", import.meta.url), + ); + const status = yield* exec(command, { arguments: [...arguments_, fixture, directory] }).join(); + expect({ code: status.code, stdout: status.stdout, stderr: status.stderr }).toMatchObject({ + code: 0, + stdout: "", + }); + expect(JSON.parse(yield* readTextFile(join(directory, "result.json")))).toEqual({ + value: "retained:7:relative-helper", + origin: pathToFileURL(direct).href, + sibling: "sibling", + projected: "continued", + late: "late-native", + resolved: pathToFileURL(join(directory, "components/late.ts")).href, + }); + expect(yield* readTextFile(join(directory, "effects.txt"))).toBe("completed\n"); + expect(yield* readTextFile(join(directory, "releases.txt"))).toBe("released\nreleased\n"); + expect(yield* readTextFile(join(directory, "continued.jsonl"))).toContain('"type":"close"'); + }); +}); diff --git a/packages/test-support/component-modules.ts b/packages/test-support/component-modules.ts new file mode 100644 index 000000000..fa1e4de6a --- /dev/null +++ b/packages/test-support/component-modules.ts @@ -0,0 +1,24 @@ +import { until } from "effection"; +import type { Operation } from "effection"; +import { cliRuntime } from "./launch.ts"; + +/** Explicitly install this fixture host's shipped component module loader. */ +export function* useComponentModuleLoader(): Operation { + switch (cliRuntime()) { + case "deno": { + const host = yield* until(import("@executablemd/core/component-modules/deno")); + yield* host.useDenoComponentModuleLoader(); + break; + } + case "node": { + const host = yield* until(import("@executablemd/core/component-modules/node")); + yield* host.useNodeComponentModuleLoader(); + break; + } + case "bun": { + const host = yield* until(import("@executablemd/core/component-modules/bun")); + yield* host.useBunComponentModuleLoader(); + break; + } + } +} diff --git a/packages/web/tests/component.test.ts b/packages/web/tests/component.test.ts index 6f8f9c7ec..1dd28a69f 100644 --- a/packages/web/tests/component.test.ts +++ b/packages/web/tests/component.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import { InMemoryStream } from "@executablemd/durable-streams"; @@ -322,6 +323,7 @@ describe("WebForm: a repository file outranks the registration", () => { }); it("prefers a repository WebForm.ts", function* () { + yield* useComponentModuleLoader(); const dir = yield* useLocalFixture(); yield* writeFile( join(dir, "WebForm.ts"), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7e2b012f..b74a64197 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,6 +436,9 @@ importers: '@effectionx/timebox': specifier: 0.4.3 version: 0.4.3(effection@4.1.0) + '@executablemd/core': + specifier: workspace:* + version: link:../core '@executablemd/durable-streams': specifier: workspace:* version: link:../durable-streams diff --git a/scripts/tests/cli-npm-bin.test.ts b/scripts/tests/cli-npm-bin.test.ts index b5992cf1f..0fff5b84c 100644 --- a/scripts/tests/cli-npm-bin.test.ts +++ b/scripts/tests/cli-npm-bin.test.ts @@ -21,6 +21,7 @@ import { exists, readTextFile, writeTextFile } from "@effectionx/fs"; import { createHash } from "node:crypto"; import type { Operation } from "effection"; import { exec, Stdio } from "@effectionx/process"; +import { symlink } from "node:fs/promises"; import type { ProcessResult } from "@effectionx/process"; import { timebox } from "@effectionx/timebox"; import { useNpmWorkspace } from "./npm-output.ts"; @@ -199,6 +200,45 @@ describe("npm CLI package", { sanitizeOps: false, sanitizeResources: false }, () throw new Error(`build-npm.ts exited ${built.code}\n${built.stderr}`); } + const consumer = yield* useTempDirectory("xmd-npm-module-consumer-"); + yield* until( + symlink( + path.join(root, PKG_DIR, "npm/node_modules"), + path.join(consumer, "node_modules"), + "dir", + ), + ); + yield* writeTextFile( + path.join(consumer, "helper.mjs"), + 'export const value = "npm-typed-component";', + ); + yield* writeTextFile( + path.join(consumer, "Typed.ts"), + 'import { value } from "./helper.mjs";\nexport default function*() { const typed: string = value; return typed; }', + ); + const probe = path.join(consumer, "consumer.mjs"); + yield* writeTextFile( + probe, + [ + 'import { main } from "effection";', + 'import { collect, execute } from "@executablemd/core";', + 'import { ComponentModuleLoader } from "@executablemd/core/api";', + 'import { InMemoryStream } from "@executablemd/durable-streams";', + ...["deno", "node", "bun"].map( + (host) => `import * as ${host} from "@executablemd/core/component-modules/${host}";`, + ), + "main(function*() {", + ' if ((yield* ComponentModuleLoader.get()) !== undefined) throw new Error("adapter import installed a loader");', + ' if (typeof deno.useDenoComponentModuleLoader !== "function" || typeof bun.useBunComponentModuleLoader !== "function") throw new Error("foreign installer missing");', + " yield* node.useNodeComponentModuleLoader();", + ` console.log(yield* collect(yield* execute({ path: "", source: "", includes: [${JSON.stringify(consumer)}], stream: new InMemoryStream() })));`, + "});", + ].join("\n"), + ); + const moduleResult = yield* exec("node", { arguments: [probe], cwd: consumer }).join(); + expect({ code: moduleResult.code, stderr: moduleResult.stderr }).toMatchObject({ code: 0 }); + expect(moduleResult.stdout).toContain("npm-typed-component"); + const run = yield* runEmittedBin(root, [ "test", path.join(root, "smoke-test/test-agent/README.md"), diff --git a/scripts/tests/component-form-dispatch.test.ts b/scripts/tests/component-form-dispatch.test.ts index 76cc13072..b43f1e5d7 100644 --- a/scripts/tests/component-form-dispatch.test.ts +++ b/scripts/tests/component-form-dispatch.test.ts @@ -38,12 +38,31 @@ interface Run { readonly output: string; } -function* run(cwd: string, args: string[]): Operation { - const settled = yield* exec(BINARY, { arguments: args, cwd }).join(); +function* run(cwd: string, args: string[], env?: Record): Operation { + const settled = yield* exec(BINARY, { arguments: args, cwd, env }).join(); return { code: settled.code ?? 1, output: `${settled.stdout}${settled.stderr}` }; } describe("compiled form dispatch across loaded copies", () => { + it("L4: loads a typed component and relative helper outside the source checkout", function* () { + const directory = yield* useTempDirectory("xmd-compiled-module-"); + yield* writeTextFile( + join(directory, "helper.ts"), + 'export const value: string = "compiled-typed-component";', + ); + yield* writeTextFile( + join(directory, "Typed.ts"), + 'import { value } from "./helper.ts";\nexport default function*() { const typed: string = value; return typed; }', + ); + const result = yield* run(directory, ["-e", "", "--include", directory, "--raw"], { + PATH: "", + }); + expect(result).toEqual({ + code: 0, + output: expect.stringContaining("compiled-typed-component"), + }); + }); + it("reads the same way with the embedded default and with a second copy", function* () { if (!(yield* exists(BINARY))) { throw new Error(`${BINARY} is missing — run \`deno task build\` before this suite`); diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 28f8bfc71..250acd57d 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -2428,6 +2428,37 @@ Function components are TypeScript files (`.ts`) that export an Effection generator function as their default export. They receive validated props directly and return rendered output as a string. +**Host installation.** The CLI installs its host's component module loader. +Programmatic callers install one explicitly in their owning Effection scope: + +```typescript +import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; + +yield* useDenoComponentModuleLoader(); +const execution = yield* execute(options); +``` + +Node uses `useNodeComponentModuleLoader` from +`@executablemd/core/component-modules/node`; Bun uses +`useBunComponentModuleLoader` from `@executablemd/core/component-modules/bun`. +Importing these subpaths installs nothing. An eval compiler does not install a +component module loader. Without one, a repository TypeScript selection refuses +before native loading with: “TypeScript component module loader is not installed +— bind ComponentModuleLoader for this host before loading TypeScript components.” +Markdown and non-file registrations remain available. + +`ComponentModuleLoader` is a same-name interface and Context exported from +`@executablemd/core/api`. Its `load(source, origin): Operation` receives +the selected direct text and absolute logical file URL and returns the complete +module namespace. Canonical setup captures the selected provider and bound +method before profile resolution; later Context bindings affect later +invocations. The loader grants no permission and writes no staging file. +Relative and bare dependencies, re-exports, computed dynamic imports and module +metadata use the logical origin. Dependencies remain live host dependencies; +only the direct selected source is retained. Native hooks and object URLs belong +to the canonical execution, survive namespace return and remain available for +later module use. Cancellation joins native loading before releasing them. + ```typescript // components/Greeting.ts import type { Json } from "@executablemd/core"; @@ -4120,6 +4151,13 @@ operation, with the read it leads to. What the journal holds is serializable: a repository selection records the chosen path and its content, and a registration records its origin, never its function. +Both profile capture and durable import materialize TypeScript from that +selection's exact direct text through the captured `ComponentModuleLoader`. +New work at the same path uses edited text. Retained continuation uses recorded +text even when the direct file changes or disappears, with the same logical +origin and ordinary live dependencies. Materialization adds no journal member +and does not repeat completed effects. + The root's selection carries one more member. A targeted root resolves its selector here too, against the text this operation is about to record, and records the **exact** target it resolved to — so the section the run executed is @@ -4172,18 +4210,11 @@ function* durableImportComponent( return found.definition; } - // Function component: .ts file — import() the module + // Direct source and the executable namespace describe the same selection. if (selection.path.endsWith(".ts")) { const absolutePath = `${process.cwd()}/${selection.path}`; - const mod = yield* call(() => import(`file://${absolutePath}`)); - return { - kind: "function" as const, - name, - props: mod.props === undefined - ? { type: "object", properties: {}, additionalProperties: false } - : parseJsonObject(mod.props), - fn: mod.default, - }; + const mod = yield* loadModule(selection.content, pathToFileURL(absolutePath).href); + return validateFunctionComponent(name, mod); } // Markdown component: parse at runtime — deterministic from content diff --git a/tsconfig.node.json b/tsconfig.node.json index cc31bc08f..5a780273d 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -3,12 +3,8 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", - "lib": [ - "ES2022" - ], - "types": [ - "node" - ], + "lib": ["ES2022"], + "types": ["node"], "strict": true, "noImplicitAny": false, "esModuleInterop": true, @@ -16,57 +12,29 @@ "noEmit": true, "allowImportingTsExtensions": true, "paths": { - "@executablemd/acp": [ - "./packages/acp/mod.ts" - ], - "@executablemd/cli": [ - "./packages/cli/src/cli.ts" - ], - "@executablemd/code-review-agent": [ - "./packages/code-review-agent/mod.ts" - ], - "@executablemd/core": [ - "./packages/core/mod.ts" - ], - "@executablemd/durable-streams": [ - "./packages/durable-streams/mod.ts" - ], - "@executablemd/runtime": [ - "./packages/runtime/mod.ts" - ], - "@executablemd/runtime/files": [ - "./packages/runtime/files.ts" - ], - "@executablemd/runtime/test": [ - "./packages/runtime/test/mod.ts" - ], - "@executablemd/test-agent": [ - "./packages/test-agent/mod.ts" - ], - "@executablemd/test-support/bdd": [ - "./packages/test-support/bdd.ts" - ], - "@executablemd/test-support/expect": [ - "./packages/test-support/expect.ts" - ], - "@executablemd/test-support/journal": [ - "./packages/test-support/journal.ts" - ], - "@executablemd/test-support/launch": [ - "./packages/test-support/launch.ts" - ], - "@executablemd/test-support/temp": [ - "./packages/test-support/temp.ts" - ], - "@executablemd/testing": [ - "./packages/testing/mod.ts" - ], - "@executablemd/web": [ - "./packages/web/mod.ts" - ], - "@executablemd/workflow": [ - "./packages/workflow/mod.ts" - ] + "@executablemd/test-support/component-modules": [ + "./packages/test-support/component-modules.ts" + ], + "@executablemd/core/component-modules/deno": ["./packages/core/component-modules/deno.ts"], + "@executablemd/core/component-modules/node": ["./packages/core/component-modules/node.ts"], + "@executablemd/core/component-modules/bun": ["./packages/core/component-modules/bun.ts"], + "@executablemd/acp": ["./packages/acp/mod.ts"], + "@executablemd/cli": ["./packages/cli/src/cli.ts"], + "@executablemd/code-review-agent": ["./packages/code-review-agent/mod.ts"], + "@executablemd/core": ["./packages/core/mod.ts"], + "@executablemd/durable-streams": ["./packages/durable-streams/mod.ts"], + "@executablemd/runtime": ["./packages/runtime/mod.ts"], + "@executablemd/runtime/files": ["./packages/runtime/files.ts"], + "@executablemd/runtime/test": ["./packages/runtime/test/mod.ts"], + "@executablemd/test-agent": ["./packages/test-agent/mod.ts"], + "@executablemd/test-support/bdd": ["./packages/test-support/bdd.ts"], + "@executablemd/test-support/expect": ["./packages/test-support/expect.ts"], + "@executablemd/test-support/journal": ["./packages/test-support/journal.ts"], + "@executablemd/test-support/launch": ["./packages/test-support/launch.ts"], + "@executablemd/test-support/temp": ["./packages/test-support/temp.ts"], + "@executablemd/testing": ["./packages/testing/mod.ts"], + "@executablemd/web": ["./packages/web/mod.ts"], + "@executablemd/workflow": ["./packages/workflow/mod.ts"] } }, "include": [ From 09c06f1a98d9282c86a538be1ca4c74812a4da1f Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Thu, 22 Oct 2026 10:33:48 -0400 Subject: [PATCH 4/5] fix(core): scope loader permission regression to its Deno host --- packages/core/package.json | 4 +- .../tests/component-module-loader.test.ts | 81 ++++++++++--------- packages/test-support/package.json | 4 +- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 613bb2c2b..8a96d782d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,6 +12,7 @@ "./component-modules/bun": "./component-modules/bun.ts" }, "dependencies": { + "@babel/standalone": "8.0.7", "@effectionx/context-api": "0.6.0", "@effectionx/converge": "0.1.4", "@effectionx/fetch": "0.2.1", @@ -38,7 +39,6 @@ "remark": "15", "remend": "^1.2.2", "unist-util-select": "^5", - "yq.rs": "0.1.4", - "@babel/standalone": "8.0.7" + "yq.rs": "0.1.4" } } diff --git a/packages/core/tests/component-module-loader.test.ts b/packages/core/tests/component-module-loader.test.ts index c3f47c734..f752f2956 100644 --- a/packages/core/tests/component-module-loader.test.ts +++ b/packages/core/tests/component-module-loader.test.ts @@ -9,6 +9,7 @@ import { createContext, ensure, scoped, spawn, suspend, until, withResolvers } f import type { Operation } from "effection"; import * as moduleApi from "node:module"; import { join } from "node:path"; +import { execPath } from "node:process"; import { fileURLToPath, pathToFileURL } from "node:url"; import { InMemoryStream } from "@executablemd/durable-streams"; import { collect, Component, execute, registerComponents, useNormalizedOutput } from "../mod.ts"; @@ -231,44 +232,46 @@ describe("ComponentModuleLoader", () => { }); }); - it("P1: loads under the preserved read-only envelope and retains authored write denial", function* () { - const directory = yield* useTempDirectory("component-loader-readonly-"); - const direct = join(directory, "ReadOnly.ts"); - yield* writeTextFile( - direct, - 'export default function*() { const value: string = "ordinary-read-only-component"; return value; }', - ); - const before = yield* readTextFile(direct); - const fixture = join(ROOT, "packages/core/tests/fixtures/component-module-readonly.ts"); - for (const mode of ["read", "write"]) { - const result = yield* exec("deno", { - arguments: [ - "run", - "--config", - join(ROOT, "deno.json"), - "--node-modules-dir=manual", - "--allow-read", - "--allow-env", - "--allow-import", - "--deny-write", - "--no-prompt", - "--cached-only", - "--frozen", - fixture, - directory, - mode, - ], - }).join(); - expect({ mode, code: result.code, stderr: result.stderr }).toEqual({ - mode, - code: 0, - stderr: "", - }); - expect(result.stdout.trim()).toBe( - mode === "read" ? "ordinary-read-only-component" : "authored-write-denied", + if (cliRuntime() === "deno") { + it("P1: Deno loads under the preserved read-only envelope and retains authored write denial", function* () { + const directory = yield* useTempDirectory("component-loader-readonly-"); + const direct = join(directory, "ReadOnly.ts"); + yield* writeTextFile( + direct, + 'export default function*() { const value: string = "ordinary-read-only-component"; return value; }', ); - } - expect(yield* readTextFile(direct)).toBe(before); - expect((yield* readdir(directory)).sort()).toEqual(["ReadOnly.ts"]); - }); + const before = yield* readTextFile(direct); + const fixture = join(ROOT, "packages/core/tests/fixtures/component-module-readonly.ts"); + for (const mode of ["read", "write"]) { + const result = yield* exec(execPath, { + arguments: [ + "run", + "--config", + join(ROOT, "deno.json"), + "--node-modules-dir=manual", + "--allow-read", + "--allow-env", + "--allow-import", + "--deny-write", + "--no-prompt", + "--cached-only", + "--frozen", + fixture, + directory, + mode, + ], + }).join(); + expect({ mode, code: result.code, stderr: result.stderr }).toEqual({ + mode, + code: 0, + stderr: "", + }); + expect(result.stdout.trim()).toBe( + mode === "read" ? "ordinary-read-only-component" : "authored-write-denied", + ); + } + expect(yield* readTextFile(direct)).toBe(before); + expect((yield* readdir(directory)).sort()).toEqual(["ReadOnly.ts"]); + }); + } }); diff --git a/packages/test-support/package.json b/packages/test-support/package.json index 574ae9c0f..6786a922f 100644 --- a/packages/test-support/package.json +++ b/packages/test-support/package.json @@ -17,9 +17,9 @@ "@effectionx/process": "0.8.1", "@effectionx/test-adapter": "0.7.4", "@effectionx/timebox": "0.4.3", + "@executablemd/core": "workspace:*", "@executablemd/durable-streams": "workspace:*", "effection": "4.1.0", - "expect": "^30.0.0", - "@executablemd/core": "workspace:*" + "expect": "^30.0.0" } } From 50e70e6426bd99dca73e9ce68191574bdd697243 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Thu, 22 Oct 2026 12:00:34 -0400 Subject: [PATCH 5/5] fix(core): retain component loads with per-load evaluation scopes --- architecture.md | 13 +- deno.json | 7 +- knip.json | 18 +-- package.json | 2 +- packages/cli/src/bun.ts | 2 +- packages/cli/src/compiled.ts | 2 +- packages/cli/src/deno.ts | 2 +- packages/cli/src/node.ts | 2 +- .../testing-execution-host-native.test.ts | 46 -------- .../cli/tests/testing-execution-host.test.ts | 27 +++++ packages/core/component-modules/bun.ts | 2 +- packages/core/component-modules/hooks.ts | 2 +- packages/core/deno.json | 6 +- packages/core/package.json | 6 +- packages/core/src/typescript-module.ts | 23 ++-- .../tests/component-module-loader.test.ts | 111 +++++++++++++++++- .../core/tests/component-registration.test.ts | 2 +- packages/core/tests/component-returns.test.ts | 2 +- packages/core/tests/contextual-cwd.test.ts | 2 +- .../fixtures/component-module-readonly.ts | 2 +- .../fixtures/typescript-materialization.ts | 2 +- .../core/tests/function-components.test.ts | 2 +- .../core/tests/generated-selection.test.ts | 2 +- .../core/tests/invocation-identity.test.ts | 2 +- .../tests/typescript-materialization.test.ts | 2 +- .../{component-modules.ts => host.ts} | 6 +- packages/test-support/launch.ts | 8 +- packages/test-support/package.json | 2 +- packages/testing/tests/smoke.test.ts | 2 + packages/web/tests/component.test.ts | 2 +- scripts/tests/cli-npm-bin.test.ts | 2 +- scripts/tests/review-infrastructure.test.ts | 46 +++++--- specs/executable-mdx-spec.md | 6 +- tsconfig.node.json | 10 +- 34 files changed, 238 insertions(+), 135 deletions(-) rename packages/test-support/{component-modules.ts => host.ts} (68%) diff --git a/architecture.md b/architecture.md index 0ac5b497b..07171e32f 100644 --- a/architecture.md +++ b/architecture.md @@ -208,7 +208,7 @@ setup captures its bound `load(source, origin)` before profile resolution. Profile and durable imports pass exact selected direct bytes and the logical file URL; core still validates the complete namespace and owns durable identity and admission. Runtime-named CLI entrypoints install their matching core -`component-modules/deno`, `/node` or `/bun` provider; compiled installs Deno's. +`host/deno`, `/node` or `/bun` provider; compiled installs Deno's. Embeddings explicitly use the matching exported installer. Eval compilation is a separate service. Absent installation refuses TypeScript loading before native work; Markdown and registered functions are unaffected. @@ -216,10 +216,13 @@ work; Markdown and registered functions are unaffected. These adapters transform direct TypeScript in memory, preserving module exports, logical origin, source maps and ordinary live dependencies without staging files or permission grants. Deno and Node retain owned native hooks; Bun retains an -owned JavaScript object URL. Loading children belong to canonical execution, -survive the short-lived materialization task and namespace return, and end after -later module use. Cancellation joins native import work before deregistration -or revocation. Retained imports execute retained direct text after file removal; +owned JavaScript object URL. Each load has a separate canonical-owned scope-eval +worker, retaining resources beyond materialization and namespace return without +serializing independent loads. Before namespace transfer, cancellation halts and +joins that worker’s hosting task; canonical completion owns transferred hosts. +Adapter teardown joins native import work before deregistration or revocation. +The transformer is imported lazily by the actual load operation; neutral imports +and adapter installation evaluate no Babel code. Retained imports execute retained direct text after file removal; they do not snapshot dependencies or repeat completed effects. A callback registered with something outside the process — a listener on a diff --git a/deno.json b/deno.json index 3b4b18680..7ce8fd7e9 100644 --- a/deno.json +++ b/deno.json @@ -1,8 +1,5 @@ { - "workspace": [ - "packages/*", - "site" - ], + "workspace": ["packages/*", "site"], "exclude": [ "scripts/tests/fixtures", ".xmd-eval", @@ -54,7 +51,7 @@ "zod": "npm:zod@^4.3.6", "mdast-util-to-string": "npm:mdast-util-to-string@^4", "yq.rs": "npm:yq.rs@0.1.4", - "@executablemd/test-support/component-modules": "./packages/test-support/component-modules.ts" + "@executablemd/test-support/host": "./packages/test-support/host.ts" }, "tasks": { "xmd": "deno run --allow-all packages/cli/src/deno.ts", diff --git a/knip.json b/knip.json index a2e565f85..a2ef777fd 100644 --- a/knip.json +++ b/knip.json @@ -30,9 +30,9 @@ ], "@executablemd/core": ["packages/core/mod.ts"], "@executablemd/core/api": ["packages/core/api.ts"], - "@executablemd/core/component-modules/deno": ["packages/core/component-modules/deno.ts"], - "@executablemd/core/component-modules/node": ["packages/core/component-modules/node.ts"], - "@executablemd/core/component-modules/bun": ["packages/core/component-modules/bun.ts"], + "@executablemd/core/host/deno": ["packages/core/component-modules/deno.ts"], + "@executablemd/core/host/node": ["packages/core/component-modules/node.ts"], + "@executablemd/core/host/bun": ["packages/core/component-modules/bun.ts"], "@executablemd/core/host": ["packages/core/host.ts"], "@executablemd/durable-streams": ["packages/durable-streams/mod.ts"], "@executablemd/git": ["packages/git/mod.ts"], @@ -74,15 +74,9 @@ ], "@executablemd/core": ["../../packages/core/mod.ts"], "@executablemd/core/api": ["../../packages/core/api.ts"], - "@executablemd/core/component-modules/deno": [ - "../../packages/core/component-modules/deno.ts" - ], - "@executablemd/core/component-modules/node": [ - "../../packages/core/component-modules/node.ts" - ], - "@executablemd/core/component-modules/bun": [ - "../../packages/core/component-modules/bun.ts" - ], + "@executablemd/core/host/deno": ["../../packages/core/component-modules/deno.ts"], + "@executablemd/core/host/node": ["../../packages/core/component-modules/node.ts"], + "@executablemd/core/host/bun": ["../../packages/core/component-modules/bun.ts"], "@executablemd/core/host": ["../../packages/core/host.ts"], "@executablemd/durable-streams": ["../../packages/durable-streams/mod.ts"], "@executablemd/git": ["../../packages/git/mod.ts"], diff --git a/package.json b/package.json index 5278159c7..60cedefe6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "type": "module", - "description": "executable.md \u2014 treat markdown documents as executable workflows.", + "description": "executable.md — treat markdown documents as executable workflows.", "homepage": "https://executable.md", "repository": { "type": "git", diff --git a/packages/cli/src/bun.ts b/packages/cli/src/bun.ts index 809a5813d..4f7d839c9 100644 --- a/packages/cli/src/bun.ts +++ b/packages/cli/src/bun.ts @@ -11,7 +11,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileTempFile } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; -import { useBunComponentModuleLoader } from "@executablemd/core/component-modules/bun"; +import { useBunComponentModuleLoader } from "@executablemd/core/host/bun"; import { useBunRepl } from "./bun-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; diff --git a/packages/cli/src/compiled.ts b/packages/cli/src/compiled.ts index 3c07fa10c..236830bc6 100644 --- a/packages/cli/src/compiled.ts +++ b/packages/cli/src/compiled.ts @@ -10,7 +10,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileDataUri } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; -import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; +import { useDenoComponentModuleLoader } from "@executablemd/core/host/deno"; import { useCompiledRepl } from "./compiled-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; diff --git a/packages/cli/src/deno.ts b/packages/cli/src/deno.ts index af475f134..7f59c373d 100644 --- a/packages/cli/src/deno.ts +++ b/packages/cli/src/deno.ts @@ -13,7 +13,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileDataUri } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; -import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; +import { useDenoComponentModuleLoader } from "@executablemd/core/host/deno"; import { useDenoRepl } from "./deno-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; diff --git a/packages/cli/src/node.ts b/packages/cli/src/node.ts index bcec09b36..8b1deee96 100755 --- a/packages/cli/src/node.ts +++ b/packages/cli/src/node.ts @@ -17,7 +17,7 @@ import process from "node:process"; import { API, useHostFiles } from "@executablemd/runtime"; import { compileTempFile } from "@executablemd/core"; import { runXmd, XMD_VERSION } from "./cli.ts"; -import { useNodeComponentModuleLoader } from "@executablemd/core/component-modules/node"; +import { useNodeComponentModuleLoader } from "@executablemd/core/host/node"; import { useNodeRepl } from "./node-repl.ts"; import { readInputStream } from "./standard-input.ts"; import { importPluginModule } from "./host-plugin-modules.ts"; diff --git a/packages/cli/tests/testing-execution-host-native.test.ts b/packages/cli/tests/testing-execution-host-native.test.ts index 0189ace23..b36626352 100644 --- a/packages/cli/tests/testing-execution-host-native.test.ts +++ b/packages/cli/tests/testing-execution-host-native.test.ts @@ -23,8 +23,6 @@ import { Execution } from "@executablemd/core/api"; import type { Plugin, PluginInstallation } from "@executablemd/core/api"; import { cwd } from "@executablemd/runtime"; import { cliBase } from "@executablemd/test-support/launch"; -import { runCli, runShell, shellQuote } from "@executablemd/test-support/launch"; -import { writeTextFile } from "@effectionx/fs"; import { useTempDirectory } from "@executablemd/test-support/temp"; import type { HelperAssembly } from "@executablemd/git/credential-helper"; import { planComponentDeclaration } from "../src/plan-component.ts"; @@ -50,50 +48,6 @@ import { denoRunRepositories } from "../src/deno-repositories.ts"; * configured. */ describe("C1 — the native host a run child initializes", () => { - it("L4: source entrypoints install the loader and carry it into detached run children", function* () { - const project = yield* useTempDirectory("xmd-module-host-"); - const repository = fileURLToPath(new URL("../../../", import.meta.url)); - yield* writeTextFile( - join(project, "helper.ts"), - 'export const value: string = "host-typed-component";', - ); - yield* writeTextFile( - join(project, "Typed.ts"), - 'import { value } from "./helper.ts";\nexport default function*() { const typed: string = value; return typed; }', - ); - yield* writeTextFile( - join(project, "README.test.md"), - [ - '', - '', - '', - '"} as="child">', - '', - '', - "", - "", - ].join("\n"), - ); - const args = ["test", "README.test.md", "--include", project]; - console.log(JSON.stringify({ argv: [...cliBase(), ...args], cwd: project })); - const deno = yield* runCli(args, { cwd: project }).join(); - expect({ code: deno.code, stderr: deno.stderr }).toEqual({ code: 0, stderr: "" }); - for (const argv of [ - [ - "tsx", - "--tsconfig", - join(repository, "tsconfig.node.json"), - join(repository, "packages/cli/src/node.ts"), - ...args, - ], - ["bun", join(repository, "packages/cli/src/bun.ts"), ...args], - ]) { - console.log(JSON.stringify({ argv, cwd: project })); - const result = yield* runShell(argv.map(shellQuote).join(" "), { cwd: project }).join(); - expect({ argv, code: result.code, stderr: result.stderr }).toMatchObject({ argv, code: 0 }); - } - }); - /** A bare remote a child may clone without reaching a network. */ function* useBareRemote(home: string): Operation { const parent = yield* until(realpath(yield* useTempDirectory("xmd-c1-remote-"))); diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index f9d8175a5..cd5ed7eff 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -1147,3 +1147,30 @@ describe("execution initialization in isolated run children", () => { expect(result.stdout + result.stderr).not.toContain("❌"); }); }); + +describe("L4 — current source host installation", () => { + it("installs the loader and carries it into detached run children", function* () { + const project = yield* useProject({ + "package.json": '{"type":"module"}', + "helper.ts": 'export const value: string = "host-typed-component";', + "Typed.ts": + 'import { value } from "./helper.ts";\nexport default function*() { const typed: string = value; return typed; }', + "README.test.md": doc( + '', + '', + '', + '"} as="child">', + '', + '', + "", + "", + ), + }); + const args = ["test", "README.test.md", "--include", project]; + console.log(JSON.stringify({ argv: [...cliBase(), ...args], cwd: project })); + const result = yield* runCli(args, { cwd: project }).join(); + expect({ code: result.code, stderr: result.stderr }).toMatchObject({ code: 0 }); + expect(result.stdout).toContain("host-typed-component"); + expect(result.stdout).toContain("AssertEquals** passed"); + }); +}); diff --git a/packages/core/component-modules/bun.ts b/packages/core/component-modules/bun.ts index 6aab123fd..686532b22 100644 --- a/packages/core/component-modules/bun.ts +++ b/packages/core/component-modules/bun.ts @@ -3,9 +3,9 @@ import type { Operation } from "effection"; import { dirname, isAbsolute } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { ComponentModuleLoader } from "../src/component-module-loader.ts"; -import { componentSource } from "./transform.ts"; function* load(source: string, origin: string): Operation { + const { componentSource } = yield* until(import("./transform.ts")); const bun: unknown = Reflect.get(globalThis, "Bun"); if ( typeof bun !== "object" || diff --git a/packages/core/component-modules/hooks.ts b/packages/core/component-modules/hooks.ts index 1225ec036..0b4969316 100644 --- a/packages/core/component-modules/hooks.ts +++ b/packages/core/component-modules/hooks.ts @@ -1,7 +1,6 @@ import { allSettled, ensure, until } from "effection"; import type { Operation } from "effection"; import type { RegisterHooksOptions, ModuleHooks } from "node:module"; -import { componentSource } from "./transform.ts"; /** Keep one host's supplied-source hooks until the consuming invocation ends. */ export function* loadHookModule( @@ -9,6 +8,7 @@ export function* loadHookModule( origin: string, register: (hooks: RegisterHooksOptions) => ModuleHooks, ): Operation { + const { componentSource } = yield* until(import("./transform.ts")); const digest = new Uint8Array( yield* until(crypto.subtle.digest("SHA-256", new TextEncoder().encode(source))), ); diff --git a/packages/core/deno.json b/packages/core/deno.json index 7b69fd6b9..40db0cdd3 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -5,9 +5,9 @@ ".": "./mod.ts", "./api": "./api.ts", "./host": "./host.ts", - "./component-modules/deno": "./component-modules/deno.ts", - "./component-modules/node": "./component-modules/node.ts", - "./component-modules/bun": "./component-modules/bun.ts" + "./host/deno": "./component-modules/deno.ts", + "./host/node": "./component-modules/node.ts", + "./host/bun": "./component-modules/bun.ts" }, "imports": { "@effectionx/context-api": "npm:@effectionx/context-api@0.6.0", diff --git a/packages/core/package.json b/packages/core/package.json index 8a96d782d..c63d0ebea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,9 +7,9 @@ ".": "./mod.ts", "./api": "./api.ts", "./host": "./host.ts", - "./component-modules/deno": "./component-modules/deno.ts", - "./component-modules/node": "./component-modules/node.ts", - "./component-modules/bun": "./component-modules/bun.ts" + "./host/deno": "./component-modules/deno.ts", + "./host/node": "./component-modules/node.ts", + "./host/bun": "./component-modules/bun.ts" }, "dependencies": { "@babel/standalone": "8.0.7", diff --git a/packages/core/src/typescript-module.ts b/packages/core/src/typescript-module.ts index d321cd0d6..e72634280 100644 --- a/packages/core/src/typescript-module.ts +++ b/packages/core/src/typescript-module.ts @@ -1,5 +1,7 @@ -import { ensure, Err, Ok, scoped, suspend, useScope, withResolvers } from "effection"; -import type { Operation, Result, Task } from "effection"; +import { ensure, scoped, suspend, useScope, withResolvers } from "effection"; +import type { Operation, Task } from "effection"; +import { unbox, useEvalScope } from "@effectionx/scope-eval"; +import type { EvalScope } from "@effectionx/scope-eval"; import { ComponentModuleLoader } from "./component-module-loader.ts"; export type LoadComponentModule = (source: string, origin: string) => Operation; @@ -17,7 +19,7 @@ export function* useComponentModules(): Operation { "TypeScript component module loader is not installed — bind ComponentModuleLoader for this host before loading TypeScript components.", ); } - const ready = withResolvers>(); + const ready = withResolvers(); let child: Task | undefined; let transferred = false; yield* ensure(function* () { @@ -26,22 +28,17 @@ export function* useComponentModules(): Operation { } }); child = owner.run(function* () { - let value: unknown; try { - value = yield* load(source, origin); + ready.resolve(yield* useEvalScope()); } catch (error) { - ready.resolve(Err(error)); + ready.reject(error instanceof Error ? error : new Error(String(error))); return; } - ready.resolve(Ok(value)); yield* suspend(); }); - const result = yield* ready.operation; - if (!result.ok) { - yield* child; - throw result.error; - } + const evaluation = yield* ready.operation; + const namespace = unbox(yield* evaluation.eval(() => load(source, origin))); transferred = true; - return result.value; + return namespace; }); } diff --git a/packages/core/tests/component-module-loader.test.ts b/packages/core/tests/component-module-loader.test.ts index f752f2956..6eac39456 100644 --- a/packages/core/tests/component-module-loader.test.ts +++ b/packages/core/tests/component-module-loader.test.ts @@ -1,7 +1,7 @@ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import { useTempDirectory } from "@executablemd/test-support/temp"; -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; import { cliRuntime } from "@executablemd/test-support/launch"; import { readTextFile, readdir, writeTextFile } from "@effectionx/fs"; import { exec } from "@effectionx/process"; @@ -14,6 +14,8 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { InMemoryStream } from "@executablemd/durable-streams"; import { collect, Component, execute, registerComponents, useNormalizedOutput } from "../mod.ts"; import { ComponentModuleLoader, Execution } from "../api.ts"; +import { useComponentModules } from "../src/typescript-module.ts"; +import type { LoadComponentModule } from "../src/typescript-module.ts"; import { useTempFileCompiler } from "../src/temp-file-compiler.ts"; import { loadHookModule } from "../component-modules/hooks.ts"; @@ -232,6 +234,113 @@ describe("ComponentModuleLoader", () => { }); }); + it("L3: canonical per-load evaluation keeps independent loads parallel and joins a cancelled waiter", function* () { + yield* useComponentModuleLoader(); + const native = yield* ComponentModuleLoader.get(); + if (native === undefined) { + throw new Error("fixture installer selected no loader"); + } + let installed = 0; + let removed = 0; + let load = native.load.bind(native); + if (cliRuntime() === "bun") { + const create = URL.createObjectURL; + const revoke = URL.revokeObjectURL; + yield* ensure(function* () { + URL.createObjectURL = create; + URL.revokeObjectURL = revoke; + }); + URL.createObjectURL = (blob) => { + installed++; + return create(blob); + }; + URL.revokeObjectURL = (url) => { + removed++; + revoke(url); + }; + } else { + load = (source, origin) => + loadHookModule(source, origin, (options) => { + const handle = moduleApi.registerHooks(options); + installed++; + const deregister = handle.deregister.bind(handle); + return new Proxy(handle, { + get(target, property, receiver) { + return property === "deregister" + ? () => { + removed++; + deregister(); + } + : Reflect.get(target, property, receiver); + }, + }); + }); + } + yield* ComponentModuleLoader.set({ load }); + const directory = yield* useTempDirectory("component-loader-canonical-"); + yield* writeTextFile( + join(directory, "late.mjs"), + 'export const value = "independent-late-module";', + ); + const gate = `canonical-module-${directory}`; + const entered = withResolvers(); + let release: () => void = () => { + throw new Error("native load has not entered"); + }; + yield* ensure(function* () { + Reflect.deleteProperty(globalThis, gate); + }); + Reflect.set(globalThis, gate, (resolve: () => void) => { + release = resolve; + entered.resolve(); + }); + const ready = withResolvers(); + const canonical = yield* spawn(function* () { + ready.resolve(yield* useComponentModules()); + yield* suspend(); + }); + const captured = yield* ready.operation; + let firstReturned = false; + const first = yield* spawn(function* () { + yield* captured( + `await new Promise(resolve => Reflect.get(globalThis, ${JSON.stringify(gate)})(resolve));\nexport const value = "held";`, + pathToFileURL(join(directory, "Held.ts")).href, + ); + firstReturned = true; + }); + yield* entered.operation; + const second = yield* captured( + 'export const later = () => import("./late.mjs");', + pathToFileURL(join(directory, "Independent.ts")).href, + ); + if ( + typeof second !== "object" || + second === null || + !("later" in second) || + typeof second.later !== "function" + ) { + throw new Error("independent module lost its late export"); + } + expect(yield* until(second.later())).toMatchObject({ value: "independent-late-module" }); + expect(installed).toBe(2); + expect(removed).toBe(0); + let halted = false; + const stopping = yield* spawn(function* () { + yield* first.halt(); + halted = true; + }); + expect(halted).toBe(false); + expect(removed).toBe(0); + release(); + yield* stopping; + expect(halted).toBe(true); + expect(firstReturned).toBe(false); + expect(removed).toBe(1); + expect(yield* until(second.later())).toMatchObject({ value: "independent-late-module" }); + yield* canonical.halt(); + expect(removed).toBe(2); + }); + if (cliRuntime() === "deno") { it("P1: Deno loads under the preserved read-only envelope and retains authored write denial", function* () { const directory = yield* useTempDirectory("component-loader-readonly-"); diff --git a/packages/core/tests/component-registration.test.ts b/packages/core/tests/component-registration.test.ts index 8c508dea5..b8f067576 100644 --- a/packages/core/tests/component-registration.test.ts +++ b/packages/core/tests/component-registration.test.ts @@ -1,4 +1,4 @@ -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; /** * Tier CR — component registration and resolution (spec §5.3). * diff --git a/packages/core/tests/component-returns.test.ts b/packages/core/tests/component-returns.test.ts index b00ccb5ca..eb63deeff 100644 --- a/packages/core/tests/component-returns.test.ts +++ b/packages/core/tests/component-returns.test.ts @@ -1,4 +1,4 @@ -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; /** * Tier RV — component and root return values (spec §6.10, §5.4). * diff --git a/packages/core/tests/contextual-cwd.test.ts b/packages/core/tests/contextual-cwd.test.ts index d205fef2b..ddeb3a6e2 100644 --- a/packages/core/tests/contextual-cwd.test.ts +++ b/packages/core/tests/contextual-cwd.test.ts @@ -1,4 +1,4 @@ -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; /** * Tier CW — contextual working directory (spec §1.2). * diff --git a/packages/core/tests/fixtures/component-module-readonly.ts b/packages/core/tests/fixtures/component-module-readonly.ts index 5cf8b561e..7b57d09a7 100644 --- a/packages/core/tests/fixtures/component-module-readonly.ts +++ b/packages/core/tests/fixtures/component-module-readonly.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { writeTextFile } from "@effectionx/fs"; import { collect, execute, useNormalizedOutput } from "@executablemd/core"; import { InMemoryStream } from "@executablemd/durable-streams"; -import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; +import { useDenoComponentModuleLoader } from "@executablemd/core/host/deno"; main(function* () { const directory = argv[2]; diff --git a/packages/core/tests/fixtures/typescript-materialization.ts b/packages/core/tests/fixtures/typescript-materialization.ts index 89e5c9454..193e8f97f 100644 --- a/packages/core/tests/fixtures/typescript-materialization.ts +++ b/packages/core/tests/fixtures/typescript-materialization.ts @@ -8,7 +8,7 @@ import { import { collect, execute, registerComponents, useNormalizedOutput } from "@executablemd/core"; import { join } from "node:path"; import { argv } from "node:process"; -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; main(function* () { yield* useComponentModuleLoader(); diff --git a/packages/core/tests/function-components.test.ts b/packages/core/tests/function-components.test.ts index 1dd21fbca..b18726881 100644 --- a/packages/core/tests/function-components.test.ts +++ b/packages/core/tests/function-components.test.ts @@ -16,7 +16,7 @@ import type { Json } from "@executablemd/durable-streams"; import { useTempFileCompiler } from "../src/temp-file-compiler.ts"; import { ensureDir, exists, readTextFile, writeTextFile } from "@effectionx/fs"; import { useTempDirectory } from "@executablemd/test-support/temp"; -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; import { until } from "effection"; import { symlink } from "node:fs/promises"; import * as path from "node:path"; diff --git a/packages/core/tests/generated-selection.test.ts b/packages/core/tests/generated-selection.test.ts index 9c5e230c8..040b0559e 100644 --- a/packages/core/tests/generated-selection.test.ts +++ b/packages/core/tests/generated-selection.test.ts @@ -1,4 +1,4 @@ -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; /** * Tier GS — the component forms a host offers generated XMD (spec §5.3.2). * diff --git a/packages/core/tests/invocation-identity.test.ts b/packages/core/tests/invocation-identity.test.ts index 86e894b95..c3d88ecac 100644 --- a/packages/core/tests/invocation-identity.test.ts +++ b/packages/core/tests/invocation-identity.test.ts @@ -1,4 +1,4 @@ -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; /** * Tier CIV — the identity a trusted host's component names its work after * (specs/executable-mdx-spec.md §5.6). diff --git a/packages/core/tests/typescript-materialization.test.ts b/packages/core/tests/typescript-materialization.test.ts index e723b09a0..861c45463 100644 --- a/packages/core/tests/typescript-materialization.test.ts +++ b/packages/core/tests/typescript-materialization.test.ts @@ -2,7 +2,7 @@ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import { useTempDirectory } from "@executablemd/test-support/temp"; import { cliBase } from "@executablemd/test-support/launch"; -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; import { ensureDir, readTextFile, rm, writeTextFile } from "@effectionx/fs"; import { exec } from "@effectionx/process"; import { scoped, spawn, suspend, withResolvers } from "effection"; diff --git a/packages/test-support/component-modules.ts b/packages/test-support/host.ts similarity index 68% rename from packages/test-support/component-modules.ts rename to packages/test-support/host.ts index fa1e4de6a..666018efc 100644 --- a/packages/test-support/component-modules.ts +++ b/packages/test-support/host.ts @@ -6,17 +6,17 @@ import { cliRuntime } from "./launch.ts"; export function* useComponentModuleLoader(): Operation { switch (cliRuntime()) { case "deno": { - const host = yield* until(import("@executablemd/core/component-modules/deno")); + const host = yield* until(import("@executablemd/core/host/deno")); yield* host.useDenoComponentModuleLoader(); break; } case "node": { - const host = yield* until(import("@executablemd/core/component-modules/node")); + const host = yield* until(import("@executablemd/core/host/node")); yield* host.useNodeComponentModuleLoader(); break; } case "bun": { - const host = yield* until(import("@executablemd/core/component-modules/bun")); + const host = yield* until(import("@executablemd/core/host/bun")); yield* host.useBunComponentModuleLoader(); break; } diff --git a/packages/test-support/launch.ts b/packages/test-support/launch.ts index c7cfc37ab..14580bf2b 100644 --- a/packages/test-support/launch.ts +++ b/packages/test-support/launch.ts @@ -40,7 +40,13 @@ export function cliBase(): string[] { } // A fresh tsx process rather than the running one: a CLI subprocess must not // inherit --test or the runner's loaders from process.execArgv. - return ["tsx", "--tsconfig", join(ROOT, "tsconfig.node.json"), entry("node")]; + return [ + process.execPath, + fileURLToPath(import.meta.resolve("tsx/cli")), + "--tsconfig", + join(ROOT, "tsconfig.node.json"), + entry("node"), + ]; } export function cliCommand(args: string[]): { command: string; arguments: string[] } { diff --git a/packages/test-support/package.json b/packages/test-support/package.json index 6786a922f..dd0117448 100644 --- a/packages/test-support/package.json +++ b/packages/test-support/package.json @@ -10,7 +10,7 @@ "./journal": "./journal.ts", "./launch": "./launch.ts", "./temp": "./temp.ts", - "./component-modules": "./component-modules.ts" + "./host": "./host.ts" }, "dependencies": { "@effectionx/fs": "0.3.0", diff --git a/packages/testing/tests/smoke.test.ts b/packages/testing/tests/smoke.test.ts index bf83f4414..3b9ff47e5 100644 --- a/packages/testing/tests/smoke.test.ts +++ b/packages/testing/tests/smoke.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/host"; import { describe, it, beforeAll } from "@executablemd/test-support/bdd"; import { useTempFileCompiler } from "@executablemd/core"; import { expect } from "@executablemd/test-support/expect"; @@ -100,6 +101,7 @@ interface SmokeSession { function* runSmokeSession(options: ExecuteOptions): Operation { return yield* scoped(function* () { + yield* useComponentModuleLoader(); yield* useStubService(Object.freeze({ hostname: SERVICE_HOSTNAME, port: 45_678 })); const tests = yield* useTesting(); const execution = yield* execute(options); diff --git a/packages/web/tests/component.test.ts b/packages/web/tests/component.test.ts index 1dd28a69f..0926e4c73 100644 --- a/packages/web/tests/component.test.ts +++ b/packages/web/tests/component.test.ts @@ -1,4 +1,4 @@ -import { useComponentModuleLoader } from "@executablemd/test-support/component-modules"; +import { useComponentModuleLoader } from "@executablemd/test-support/host"; import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import { InMemoryStream } from "@executablemd/durable-streams"; diff --git a/scripts/tests/cli-npm-bin.test.ts b/scripts/tests/cli-npm-bin.test.ts index 0fff5b84c..11f6661bf 100644 --- a/scripts/tests/cli-npm-bin.test.ts +++ b/scripts/tests/cli-npm-bin.test.ts @@ -225,7 +225,7 @@ describe("npm CLI package", { sanitizeOps: false, sanitizeResources: false }, () 'import { ComponentModuleLoader } from "@executablemd/core/api";', 'import { InMemoryStream } from "@executablemd/durable-streams";', ...["deno", "node", "bun"].map( - (host) => `import * as ${host} from "@executablemd/core/component-modules/${host}";`, + (host) => `import * as ${host} from "@executablemd/core/host/${host}";`, ), "main(function*() {", ' if ((yield* ComponentModuleLoader.get()) !== undefined) throw new Error("adapter import installed a loader");', diff --git a/scripts/tests/review-infrastructure.test.ts b/scripts/tests/review-infrastructure.test.ts index 757bc00b1..5d39194ae 100644 --- a/scripts/tests/review-infrastructure.test.ts +++ b/scripts/tests/review-infrastructure.test.ts @@ -1,3 +1,4 @@ +import { useComponentModuleLoader } from "@executablemd/test-support/host"; import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import { FetchApi, type FetchResponse } from "@effectionx/fetch"; @@ -190,6 +191,7 @@ function* runDocumentResult( }, }); } + yield* useComponentModuleLoader(); yield* useStubFs({ "doc.md": document, ...components }); const process = options.process; if (process) { @@ -238,13 +240,13 @@ const OPERATIONAL_DIRS = [ /** * Read operational components from the repository before `useStubFs` replaces * the filesystem, so a run expands the shipped review documents rather than a - * copy. A `.ts` component is imported from its real path, so its stub entry - * only has to make the path resolve. + * copy. TypeScript selections retain these exact bytes while their dependencies + * resolve from their physical origin. */ function* operationalComponents(paths: string[]): Operation> { const components: Record = {}; for (const path of paths) { - components[path] = path.endsWith(".ts") ? "" : yield* readTextFile(path); + components[path] = yield* readTextFile(path); } return components; } @@ -316,7 +318,9 @@ describe("review infrastructure", () => { const valid = yield* runDocumentResult( '\n```ts eval\nconst summary = `${diagnostics.length}:${diagnostics[0].ruleId}`;\n```\n{summary}', - { "packages/code-review-agent/src/components/OxlintDiagnostics.ts": "" }, + yield* operationalComponents([ + "packages/code-review-agent/src/components/OxlintDiagnostics.ts", + ]), { includes: [ "packages/code-review-agent/src/documents/components", @@ -361,7 +365,9 @@ describe("review infrastructure", () => { yield* useTempFileCompiler(); const result = yield* runDocumentResult( '', - { "packages/code-review-agent/src/components/OxlintDiagnostics.ts": "" }, + yield* operationalComponents([ + "packages/code-review-agent/src/components/OxlintDiagnostics.ts", + ]), { includes: OPERATIONAL_DIRS, process: scenario.process, @@ -384,9 +390,9 @@ describe("review infrastructure", () => { expect( yield* runDocument( '', - { - "packages/code-review-agent/src/components/OxlintDiagnostics.ts": "", - }, + yield* operationalComponents([ + "packages/code-review-agent/src/components/OxlintDiagnostics.ts", + ]), { includes: [ "packages/code-review-agent/src/documents/components", @@ -473,7 +479,9 @@ describe("review infrastructure", () => { }; const result = yield* runDocumentResult( document, - { "packages/code-review-agent/src/components/CommentReviewData.ts": "" }, + yield* operationalComponents([ + "packages/code-review-agent/src/components/CommentReviewData.ts", + ]), options, ); expect(result.ok).toBe(true); @@ -484,7 +492,9 @@ describe("review infrastructure", () => { malformed = true; const malformedResult = yield* runDocumentResult( document, - { "packages/code-review-agent/src/components/CommentReviewData.ts": "" }, + yield* operationalComponents([ + "packages/code-review-agent/src/components/CommentReviewData.ts", + ]), options, ); expect(malformedResult.ok).toBe(false); @@ -513,7 +523,9 @@ describe("review infrastructure", () => { ].join("\n"); const result = yield* runDocumentResult( document, - { "packages/code-review-agent/src/components/CommentReviewState.ts": "" }, + yield* operationalComponents([ + "packages/code-review-agent/src/components/CommentReviewState.ts", + ]), { includes: [ "packages/code-review-agent/src/documents/components", @@ -541,7 +553,9 @@ describe("review infrastructure", () => { "{summary}", "", ].join("\n"); - const component = { "packages/code-review-agent/src/components/Doctor.ts": "" }; + const component = yield* operationalComponents([ + "packages/code-review-agent/src/components/Doctor.ts", + ]); const stats = (available: boolean) => (path: string) => { if (path === ".reviews/.oxlint/oxlint" || path === ".reviews/.oxlint/tsgolint") { return { exists: available, isFile: available, isDirectory: false }; @@ -646,7 +660,7 @@ describe("review infrastructure", () => { ].join("\n"); const result = yield* runDocumentResult( document, - { "packages/code-review-agent/src/components/ReviewContext.ts": "" }, + yield* operationalComponents(["packages/code-review-agent/src/components/ReviewContext.ts"]), { includes: [ "packages/code-review-agent/src/documents/components", @@ -664,7 +678,7 @@ describe("review infrastructure", () => { const failed = yield* runDocumentResult( document, - { "packages/code-review-agent/src/components/ReviewContext.ts": "" }, + yield* operationalComponents(["packages/code-review-agent/src/components/ReviewContext.ts"]), { includes: [ "packages/code-review-agent/src/documents/components", @@ -739,7 +753,9 @@ describe("review infrastructure", () => { const inventoryResult = yield* runDocumentResult( '\n{inventory.fileCount}:{inventory.lineCount}', { - "packages/code-review-agent/src/components/RepositoryInventory.ts": "", + ...(yield* operationalComponents([ + "packages/code-review-agent/src/components/RepositoryInventory.ts", + ])), "packages/example.ts": "first\nsecond\n", }, { diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 250acd57d..effec5995 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -2432,15 +2432,15 @@ validated props directly and return rendered output as a string. Programmatic callers install one explicitly in their owning Effection scope: ```typescript -import { useDenoComponentModuleLoader } from "@executablemd/core/component-modules/deno"; +import { useDenoComponentModuleLoader } from "@executablemd/core/host/deno"; yield* useDenoComponentModuleLoader(); const execution = yield* execute(options); ``` Node uses `useNodeComponentModuleLoader` from -`@executablemd/core/component-modules/node`; Bun uses -`useBunComponentModuleLoader` from `@executablemd/core/component-modules/bun`. +`@executablemd/core/host/node`; Bun uses +`useBunComponentModuleLoader` from `@executablemd/core/host/bun`. Importing these subpaths installs nothing. An eval compiler does not install a component module loader. Without one, a repository TypeScript selection refuses before native loading with: “TypeScript component module loader is not installed diff --git a/tsconfig.node.json b/tsconfig.node.json index 5a780273d..87b53e8ce 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -12,12 +12,10 @@ "noEmit": true, "allowImportingTsExtensions": true, "paths": { - "@executablemd/test-support/component-modules": [ - "./packages/test-support/component-modules.ts" - ], - "@executablemd/core/component-modules/deno": ["./packages/core/component-modules/deno.ts"], - "@executablemd/core/component-modules/node": ["./packages/core/component-modules/node.ts"], - "@executablemd/core/component-modules/bun": ["./packages/core/component-modules/bun.ts"], + "@executablemd/test-support/host": ["./packages/test-support/host.ts"], + "@executablemd/core/host/deno": ["./packages/core/component-modules/deno.ts"], + "@executablemd/core/host/node": ["./packages/core/component-modules/node.ts"], + "@executablemd/core/host/bun": ["./packages/core/component-modules/bun.ts"], "@executablemd/acp": ["./packages/acp/mod.ts"], "@executablemd/cli": ["./packages/cli/src/cli.ts"], "@executablemd/code-review-agent": ["./packages/code-review-agent/mod.ts"],