diff --git a/architecture.md b/architecture.md index 8ea01380e..07171e32f 100644 --- a/architecture.md +++ b/architecture.md @@ -201,6 +201,30 @@ 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 +`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. + +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. 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 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/deno.json b/deno.json index 2219f7c1e..7ce8fd7e9 100644 --- a/deno.json +++ b/deno.json @@ -50,7 +50,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/host": "./packages/test-support/host.ts" }, "tasks": { "xmd": "deno run --allow-all packages/cli/src/deno.ts", 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/knip.json b/knip.json index e7c33dfe7..a2ef777fd 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/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"], @@ -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,9 @@ ], "@executablemd/core": ["../../packages/core/mod.ts"], "@executablemd/core/api": ["../../packages/core/api.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 754c8c650..60cedefe6 100644 --- a/package.json +++ b/package.json @@ -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/cli/src/bun.ts b/packages/cli/src/bun.ts index bd7d40bf8..4f7d839c9 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/host/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..236830bc6 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/host/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..7f59c373d 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/host/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..8b1deee96 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/host/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.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/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..686532b22 --- /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"; + +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" || + 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..0b4969316 --- /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"; + +/** 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 { componentSource } = yield* until(import("./transform.ts")); + 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/deno.json b/packages/core/deno.json index 955fa0756..40db0cdd3 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", + "./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", @@ -23,6 +26,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..c63d0ebea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -6,9 +6,13 @@ "exports": { ".": "./mod.ts", "./api": "./api.ts", - "./host": "./host.ts" + "./host": "./host.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", "@effectionx/context-api": "0.6.0", "@effectionx/converge": "0.1.4", "@effectionx/fetch": "0.2.1", 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..e72634280 --- /dev/null +++ b/packages/core/src/typescript-module.ts @@ -0,0 +1,44 @@ +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; + +/** 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* () { + try { + ready.resolve(yield* useEvalScope()); + } catch (error) { + ready.reject(error instanceof Error ? error : new Error(String(error))); + return; + } + yield* suspend(); + }); + const evaluation = yield* ready.operation; + const namespace = unbox(yield* evaluation.eval(() => load(source, origin))); + transferred = true; + return namespace; + }); +} 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..6eac39456 --- /dev/null +++ b/packages/core/tests/component-module-loader.test.ts @@ -0,0 +1,386 @@ +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/host"; +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 { 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"; +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"; + +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("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-"); + 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(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/core/tests/component-registration.test.ts b/packages/core/tests/component-registration.test.ts index 42694c665..b8f067576 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/host"; /** * 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..eb63deeff 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/host"; /** * 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..ddeb3a6e2 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/host"; /** * 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..7b57d09a7 --- /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/host/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..193e8f97f --- /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/host"; + +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..b18726881 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/host"; 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..040b0559e 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/host"; /** * 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..c3d88ecac 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/host"; /** * 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..861c45463 --- /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/host"; +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/host.ts b/packages/test-support/host.ts new file mode 100644 index 000000000..666018efc --- /dev/null +++ b/packages/test-support/host.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/host/deno")); + yield* host.useDenoComponentModuleLoader(); + break; + } + case "node": { + const host = yield* until(import("@executablemd/core/host/node")); + yield* host.useNodeComponentModuleLoader(); + break; + } + case "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 09e5dd014..dd0117448 100644 --- a/packages/test-support/package.json +++ b/packages/test-support/package.json @@ -9,13 +9,15 @@ "./host-boundary": "./host-boundary.ts", "./journal": "./journal.ts", "./launch": "./launch.ts", - "./temp": "./temp.ts" + "./temp": "./temp.ts", + "./host": "./host.ts" }, "dependencies": { "@effectionx/fs": "0.3.0", "@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" 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 6f8f9c7ec..0926e4c73 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/host"; 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 5aa5dc4c2..b74a64197 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) @@ -430,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 @@ -557,10 +566,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 +1103,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 +2075,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 +2090,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 +2225,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 +2315,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 +2358,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 +2452,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 +2466,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 +2513,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 +2559,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 +2569,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 +2751,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 +2762,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 +3089,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 +3119,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 +3195,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 +3612,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 +4293,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 +4309,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 +4423,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 +4511,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 +4543,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 +4670,8 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + flru@1.0.2: {} + formatly@0.11.4: dependencies: fd-package-json: 2.0.0 @@ -4401,6 +4680,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 +4735,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 +4790,8 @@ snapshots: jiti@2.7.0: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -4514,8 +4799,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 +5096,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 +5515,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 +5534,8 @@ snapshots: uuid@14.0.1: {} + verkit@0.5.0: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/scripts/tests/cli-npm-bin.test.ts b/scripts/tests/cli-npm-bin.test.ts index b5992cf1f..11f6661bf 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/host/${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/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 28f8bfc71..effec5995 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/host/deno"; + +yield* useDenoComponentModuleLoader(); +const execution = yield* execute(options); +``` + +Node uses `useNodeComponentModuleLoader` from +`@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 +— 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..87b53e8ce 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,27 @@ "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/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"], + "@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": [