Skip to content

Commit 6ff94cc

Browse files
feat(jco): detect older wasi:http version for older componentizejs
1 parent 8374daa commit 6ff94cc

File tree

23 files changed

+1839
-28
lines changed

23 files changed

+1839
-28
lines changed

crates/wasm-tools-component/src/lib.rs

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mod bindings {
1717

1818
use bindings::exports::local::wasm_tools::tools::{
1919
EmbedOpts, EnabledFeatureSet, Guest, InterfaceMetadata, ModuleMetaType, ModuleMetadata,
20-
ProducersFields, SemverVersion, StringEncoding, WitMetadata,
20+
ProducersFields, SemverVersion, StringEncoding, WitMetadata, WitSpecifier,
2121
};
2222

2323
struct WasmToolsJs;
@@ -73,32 +73,39 @@ impl Guest for WasmToolsJs {
7373
}
7474

7575
fn component_wit_metadata_for_world(
76-
binary: Vec<u8>,
76+
wit: WitSpecifier,
7777
maybe_world_name: Option<String>,
7878
) -> Result<WitMetadata, String> {
79-
let decoded = wit_component::decode(&binary)
80-
.map_err(|e| format!("Failed to decode wit component\n{e:?}"))?;
81-
82-
let (resolve, world_id) = match &decoded {
83-
DecodedWasm::WitPackage(_, _) => panic!("Unexpected wit package"),
84-
DecodedWasm::Component(resolve, world) => (resolve, world),
85-
};
79+
let mut resolve = Resolve::default();
8680

87-
let world = match maybe_world_name {
88-
Some(world_name) => {
81+
// Resolve package IDs & build Resolve for WIT
82+
let package_ids = match wit {
83+
WitSpecifier::Source(source) => {
84+
let path = PathBuf::from("component.wit");
8985
resolve
90-
.worlds
91-
.iter()
92-
.find(|w| w.1.name == world_name)
93-
.ok_or_else(|| String::from("failed to find wold with given name"))?
94-
.1
86+
.push_str(&path, &source)
87+
.map_err(|e| e.to_string())?
88+
}
89+
WitSpecifier::Path(path) => {
90+
let wit_path_meta =
91+
metadata(&path).map_err(|e| format!("failed to get path metadata: {e}"))?;
92+
if wit_path_meta.is_file() {
93+
resolve.push_file(path).map_err(|e| e.to_string())?
94+
} else {
95+
resolve.push_dir(path).map_err(|e| e.to_string())?.0
96+
}
9597
}
96-
None => resolve
97-
.worlds
98-
.get(*world_id)
99-
.ok_or_else(|| String::from("failed to find package id"))?,
10098
};
10199

100+
let world_id = resolve
101+
.select_world(&[package_ids], maybe_world_name.as_deref())
102+
.map_err(|e| e.to_string())?;
103+
let world = resolve
104+
.worlds
105+
.get(world_id)
106+
.ok_or_else(|| String::from("failed to find package id"))?;
107+
108+
// Gather imports
102109
let mut imports = Vec::new();
103110
for (import_key, import_item) in world.imports.iter() {
104111
let WorldItem::Interface { id, .. } = import_item else {
@@ -140,6 +147,7 @@ impl Guest for WasmToolsJs {
140147
});
141148
}
142149

150+
// Gather exports
143151
let mut exports = Vec::new();
144152
for (export_key, export_item) in world.exports.iter() {
145153
let WorldItem::Interface { id, .. } = export_item else {

crates/wasm-tools-component/wit/wasm-tools.wit

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,21 @@ interface tools {
4343
exports: list<interface-metadata>,
4444
}
4545

46+
/// Way to specify WIT
47+
variant wit-specifier {
48+
/// WIT raw source
49+
source(string),
50+
/// Path to wit file or directory
51+
path(string),
52+
}
53+
4654
/// Extract metadata from a WIT interface for a component for a given world
4755
///
4856
/// If no world is specified, the root world is used
49-
component-wit-metadata-for-world: func(wit-bytes: list<u8>, world-name: option<string>) -> result<wit-metadata, string>;
57+
component-wit-metadata-for-world: func(
58+
wit: wit-specifier,
59+
world-name: option<string>
60+
) -> result<wit-metadata, string>;
5061

5162
type producers-fields = list<tuple<string, list<tuple<string, string>>>>;
5263

packages/jco/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
"dependencies": {
7676
"@bytecodealliance/componentize-js": "^0.20.0",
7777
"@bytecodealliance/componentize-js-0-19-3": "npm:@bytecodealliance/componentize-js@^0.19.3",
78-
"@bytecodealliance/componentize-js": "^0.20.0",
7978
"@bytecodealliance/preview2-shim": "^0.17.9",
8079
"binaryen": "^123.0.0",
8180
"commander": "^14",

packages/jco/src/cmd/componentize.js

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { stat, readFile, writeFile } from "node:fs/promises";
22
import { resolve, basename } from "node:path";
3+
4+
import * as wasmToolsComponent from "../../obj/wasm-tools.js";
5+
36
import { styleText } from "../common.js";
47

58
/** All features that can be enabled/disabled */
@@ -8,19 +11,60 @@ const ALL_FEATURES = ["clocks", "http", "random", "stdio", "fetch-event"];
811
/** Features that should be used for --debug mode */
912
const DEBUG_FEATURES = ["stdio"];
1013

11-
export async function componentize(jsSource, opts) {
12-
const { componentize: componentizeFn } = await eval('import("@bytecodealliance/componentize-js")');
14+
/**
15+
* Detect whether the WIT of a given component contains an older version of
16+
* `wasi:http` which necessitates an older version of `componentize-js`
17+
*
18+
* @param {string} witPath
19+
* @returns bool
20+
*/
21+
async function usesOlderWasiHTTP(witPath, worldName) {
22+
await wasmToolsComponent.$init;
23+
24+
const worldMetadata = wasmToolsComponent.tools.componentWitMetadataForWorld(
25+
{ tag: "path", val: witPath },
26+
worldName ?? null,
27+
);
1328

29+
// Check if the an old `wasi:http/incoming-handler` version is exported
30+
const exportsOldIncomingHandler = worldMetadata.exports.some((iface) => {
31+
return (
32+
iface.namespace === "wasi" &&
33+
iface.package === "http" &&
34+
iface.interface === "incoming-handler" &&
35+
iface.version !== null &&
36+
iface.version.major === 0n &&
37+
iface.version.minor < 3n &&
38+
iface.version.patch < 10n
39+
);
40+
});
41+
42+
return exportsOldIncomingHandler;
43+
}
44+
export async function componentize(jsSource, opts) {
1445
const { disableFeatures, enableFeatures } = calculateFeatureSet(opts);
1546

1647
const source = await readFile(jsSource, "utf8");
17-
1848
const witPath = resolve(opts.wit);
1949
const sourceName = basename(jsSource);
2050

51+
// Load an older version of componentize-js if we detect an older version of WASI HTTP in use
52+
// as the version that is usable is baked into the StarlingMonkey version provided by a given version
53+
// of componentize-js
54+
let componentizeJSModule;
55+
const useOldComponentizeJS = await usesOlderWasiHTTP(witPath, opts.worldName);
56+
if (useOldComponentizeJS) {
57+
// NOTE: if we were to use a version of componentize-js 0.20.0 or newer here,
58+
// the build would fail, as newer versions do not support wasi:http < 0.2.10
59+
// for fetch.
60+
componentizeJSModule = await eval('import("@bytecodealliance/componentize-js-0-19-3")');
61+
} else {
62+
componentizeJSModule = await eval('import("@bytecodealliance/componentize-js")');
63+
}
64+
2165
let component;
2266
try {
23-
const result = await componentizeFn(source, {
67+
const result = await componentizeJSModule.componentize(source, {
2468
enableAot: opts.aot,
2569
aotMinStackSizeBytes: opts.aotMinStackSizeBytes,
2670
wevalBin: opts.wevalBin,
@@ -41,7 +85,7 @@ export async function componentize(jsSource, opts) {
4185
},
4286
});
4387
if (result.debug) {
44-
console.error(`${styleText("cyan", "DEBUG")} Debug output\n${JSON.stringify(debug, null, 2)}\n`);
88+
console.error(`${styleText("cyan", "DEBUG")} Debug output\n${JSON.stringify(result.debug, null, 2)}\n`);
4589
}
4690

4791
component = result.component;

packages/jco/test/common.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export const JCO_JS_PATH = fileURLToPath(new URL("../src/jco.js", import.meta.ur
2222
/** Path to fixture components */
2323
export const COMPONENT_FIXTURES_DIR = fileURLToPath(new URL("./fixtures/components", import.meta.url));
2424

25+
/** Path to JS for fixture components */
26+
export const COMPONENT_JS_FIXTURES_DIR = fileURLToPath(new URL("./fixtures/componentize", import.meta.url));
27+
2528
/** Path to p3 related fixture components */
2629
export const P3_COMPONENT_FIXTURES_DIR = join(COMPONENT_FIXTURES_DIR, "p3");
2730

packages/jco/test/componentize.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* global Buffer */
2+
import { join } from "node:path";
3+
4+
import { suite, test, assert } from "vitest";
5+
import { COMPONENT_JS_FIXTURES_DIR } from "./common.js";
6+
import { exec, getTmpDir, jcoPath } from "./helpers.js";
7+
8+
// NOTE: we test componentization with the jco CLI to avoid
9+
// triggering errors for the the eval(import) call(s) in cmd/componentize.js
10+
//
11+
// TODO(breaking): once jco-transpile is established as a separate package and
12+
// used widely, we can switch to regular dynamic imports, as componentize-js
13+
// versions are real dependencies now.
14+
suite("componentize", () => {
15+
test.concurrent("detect older wasi:http", async () => {
16+
const jsPath = join(COMPONENT_JS_FIXTURES_DIR, "wasi-http-detection-old/component.js");
17+
const witPath = join(COMPONENT_JS_FIXTURES_DIR, "wasi-http-detection-old/wit");
18+
const outputDir = await getTmpDir();
19+
const outputPath = join(outputDir, "component.wasm");
20+
const { stderr } = await exec(jcoPath, "componentize", jsPath, "-w", witPath, "-o", outputPath);
21+
assert.strictEqual(stderr, "");
22+
});
23+
24+
test.concurrent("detect newer wasi:http", async () => {
25+
const jsPath = join(COMPONENT_JS_FIXTURES_DIR, "wasi-http-detection-new/component.js");
26+
const witPath = join(COMPONENT_JS_FIXTURES_DIR, "wasi-http-detection-new/wit");
27+
const outputDir = await getTmpDir();
28+
const outputPath = join(outputDir, "component.wasm");
29+
const { stderr } = await exec(jcoPath, "componentize", jsPath, "-w", witPath, "-o", outputPath);
30+
assert.strictEqual(stderr, "");
31+
});
32+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
addEventListener("fetch", (event) =>
2+
event.respondWith(
3+
(async () => {
4+
return new Response("Hello World");
5+
})(),
6+
),
7+
);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package jco-fixtures:wasi-http-detection;
2+
3+
world use-new {
4+
export wasi:http/incoming-handler@0.2.10;
5+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package wasi:cli@0.2.10;
2+
3+
interface stdout {
4+
use wasi:io/streams@0.2.10.{output-stream};
5+
6+
get-stdout: func() -> output-stream;
7+
}
8+
9+
interface stderr {
10+
use wasi:io/streams@0.2.10.{output-stream};
11+
12+
get-stderr: func() -> output-stream;
13+
}
14+
15+
interface stdin {
16+
use wasi:io/streams@0.2.10.{input-stream};
17+
18+
get-stdin: func() -> input-stream;
19+
}
20+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package wasi:clocks@0.2.10;
2+
3+
interface monotonic-clock {
4+
use wasi:io/poll@0.2.10.{pollable};
5+
6+
type instant = u64;
7+
8+
type duration = u64;
9+
10+
now: func() -> instant;
11+
12+
resolution: func() -> duration;
13+
14+
subscribe-instant: func(when: instant) -> pollable;
15+
16+
subscribe-duration: func(when: duration) -> pollable;
17+
}
18+
19+
interface wall-clock {
20+
record datetime {
21+
seconds: u64,
22+
nanoseconds: u32,
23+
}
24+
25+
now: func() -> datetime;
26+
27+
resolution: func() -> datetime;
28+
}
29+

0 commit comments

Comments
 (0)