Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/bzl-changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ jobs:
# iOS/macOS ObjC/Swift suites that never execute on the Linux runner.
run: ./tools/ci/run_tests.sh

- name: "Run: Standalone Tests"
run: ./tools/ci/test_standalone.sh

# Note: Linux CLI distribution tests moved to dedicated workflow:
# .github/workflows/test-cli-linux.yml
# This avoids duplication and provides better test organization.
Expand All @@ -191,6 +194,8 @@ jobs:
include:
- name: C++ Tests
task: test-cpp
- name: Standalone Tests
task: test-standalone
- name: Build Compiler
task: build-compiler
- name: Build & Export
Expand Down Expand Up @@ -242,6 +247,10 @@ jobs:
if: matrix.task == 'test-cpp'
run: ./tools/ci/run_tests.sh

- name: "Run: Standalone Tests"
if: matrix.task == 'test-standalone'
run: ./tools/ci/test_standalone.sh

- name: "Run: Build Compiler"
if: matrix.task == 'build-compiler'
run: ./tools/ci/build_compiler.sh
Expand Down
4 changes: 4 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ http_archive(

bazel_dep(name = "boringssl", version = "0.20250415.0")

# HTTP transport for the standalone runtime. Defaults to the BoringSSL above as its TLS
# backend, and to http_only, so no other protocol is compiled in.
bazel_dep(name = "curl", version = "8.12.0")

# TODO(simon): See if we can use upstream
http_archive(
name = "boost",
Expand Down
22 changes: 22 additions & 0 deletions apps/cli_http_example/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load("//bzl/valdi:valdi_cli_application.bzl", "valdi_cli_application")
load("//bzl/valdi:valdi_module.bzl", "valdi_module")

valdi_module(
name = "cli_http_example",
srcs = glob([
"**/*.ts",
]),
visibility = ["//visibility:public"],
deps = [
"//src/valdi_modules/src/valdi/valdi_core",
"//src/valdi_modules/src/valdi/valdi_http",
"//src/valdi_modules/src/valdi/valdi_standalone",
],
)

valdi_cli_application(
name = "cli_http_example_app",
script_path = "cli_http_example/index",
enable_http = True,
deps = [":cli_http_example"],
)
32 changes: 32 additions & 0 deletions apps/cli_http_example/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { beginKeepAlive, endKeepAlive } from 'valdi_core/src/utils/KeepAliveCallback';
import { HTTPClient } from 'valdi_http/src/HTTPClient';
import { ArgumentsParser } from 'valdi_standalone/src/ArgumentsParser';
import { getStandaloneRuntime } from 'valdi_standalone/src/ValdiStandalone';

const DEFAULT_URL = 'https://example.com';

const standalone = getStandaloneRuntime();

const parser = new ArgumentsParser('cli_http_example', standalone.arguments);
const urlArgument = parser.addString('--url', `URL to fetch (default ${DEFAULT_URL})`, false);
parser.parse();

const url = urlArgument.value ?? DEFAULT_URL;

const keepAlive = beginKeepAlive();

console.info(`GET ${url}`);

new HTTPClient().get(url).then(
response => {
const length = response.body ? response.body.byteLength : 0;
console.info(`OK: status ${response.statusCode}, ${length} bytes`);
endKeepAlive(keepAlive);
standalone.exit(0);
},
error => {
console.error(`FAIL: request rejected: ${error}`);
endKeepAlive(keepAlive);
standalone.exit(1);
},
);
1 change: 1 addition & 0 deletions bzl/valdi/app_templates/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ exports_files([
"AndroidLibManifest.xml",
"StartActivity.kt.tpl",
"cli_main.cpp.tpl",
"cli_main_http.cpp.tpl",
"linux_main.cpp.tpl",
])
4 changes: 3 additions & 1 deletion bzl/valdi/app_templates/cli_main.cpp.tpl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Paired with cli_main_http.cpp.tpl, which valdi_cli_application picks when enable_http is set.
#include "valdi/cli_runner/CLIRunner.hpp"

int main(int argc, const char** argv) {
return Valdi::valdiCLIRun("@VALDI_SCRIPT_PATH@", argc, argv);
// No backend, so valdi_http rejects every request with "No RequestManager set".
return Valdi::valdiCLIRun("@VALDI_SCRIPT_PATH@", argc, argv, nullptr);
}
7 changes: 7 additions & 0 deletions bzl/valdi/app_templates/cli_main_http.cpp.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Paired with cli_main.cpp.tpl, which valdi_cli_application picks when enable_http is not set.
#include "valdi/cli_runner/CLIRunner.hpp"
#include "valdi/standalone_http/CurlHTTPRequestManager.hpp"

int main(int argc, const char** argv) {
return Valdi::valdiCLIRun("@VALDI_SCRIPT_PATH@", argc, argv, Valdi::makeCurlHTTPRequestManager());
}
21 changes: 19 additions & 2 deletions bzl/valdi/valdi_cli_application.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,28 @@ def valdi_cli_application(
name,
script_path,
visibility = ["//visibility:public"],
enable_http = False,
deps = []):
""" Builds a Valdi CLI application into a single self contained binary.

Args:
name: The name of the generated binary.
script_path: The entry point script, as <module_name>/<path_without_extension>.
visibility: The visibility of the Bazel target.
enable_http: Links a libcurl backed HTTP client into the binary, which valdi_http needs at
runtime. Off by default because it pulls in curl and BoringSSL, which an application
making no network requests should not have to carry. Left off, requests reject with
"No RequestManager set".
deps: The Valdi modules to include in the build.
"""
main_target = "{}_main".format(name)

# Two whole templates rather than one with the manager spliced in, so both stay readable C++.
template = "cli_main_http.cpp.tpl" if enable_http else "cli_main.cpp.tpl"

expand_template(
name = main_target,
src = "@valdi//bzl/valdi/app_templates:cli_main.cpp.tpl",
src = "@valdi//bzl/valdi/app_templates:" + template,
output = "main.cpp",
substitutions = {
"@VALDI_SCRIPT_PATH@": script_path,
Expand All @@ -26,5 +42,6 @@ def valdi_cli_application(
deps = [
"@valdi//valdi:cli_runner",
"@valdi//src/valdi_modules/src/valdi/valdi_core:valdi_core_native",
] + get_suffixed_deps(deps, "_native"),
] + (["@valdi//valdi:valdi_standalone_http"] if enable_http else []) +
get_suffixed_deps(deps, "_native"),
)
15 changes: 15 additions & 0 deletions docs/docs/stdlib-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,21 @@ The `valdi_http` module works on:
- ✅ iOS
- ✅ Android
- ✅ Web (via polyfill)
- ✅ CLI (requires `enable_http = True`, see [Building a CLI application](./workflow-cli-application.md#making-network-requests))

Network requests are always performed asynchronously and will not block the JavaScript thread.

### CLI differences

The CLI's HTTP client is libcurl rather than a platform user agent, so a few things differ from iOS
and Android:

- **No response compression.** Responses arrive uncompressed, and a response that carries a
`Content-Encoding` this build cannot decode fails rather than handing back unreadable bytes.
- **`Accept-Encoding` and `Content-Length` request headers are ignored**, as `fetch()` ignores them
on the web. The transport sets both itself.
- **A non-standard method survives a 303 redirect** instead of becoming `GET`. Standard methods are
rewritten per RFC 9110; its body is dropped either way.
- **Certificates verify against the system trust store**, found via `SSL_CERT_FILE`, `SSL_CERT_DIR`,
`CURL_CA_BUNDLE` or `CURL_CA_PATH`, or else the usual distribution paths. If none is found, every
HTTPS request fails rather than succeeding unverified.
23 changes: 23 additions & 0 deletions docs/docs/workflow-cli-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,28 @@ valdi_cli_application(
)
```

## Making network requests

The [`valdi_http`](./stdlib-http.md) module needs a native HTTP client, and a CLI application only links one in when you ask for it. Set `enable_http = True` on `valdi_cli_application()` as well as adding `valdi_http` to your module's `deps`:

```python
valdi_module(
name = "cli_example",
srcs = glob([
"**/*.ts",
]),
deps = ["//src/valdi_modules/src/valdi/valdi_http"],
)

valdi_cli_application(
name = "cli_example_app",
script_path = "cli_example/index",
enable_http = True,
deps = [":cli_example"],
)
```

It is off by default because it links libcurl and BoringSSL into the binary, which an application that makes no network requests should not have to carry. Leaving it off still builds, and every request rejects at runtime with `No RequestManager set`.

You can then run your application using `valdi install cli`.
You can package your application into a single binary using `valdi package cli`.
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ Value HTTPRequestManagerModuleFactory::loadModule() {
parameters[1] = Value::undefined();
} else {
parameters[0] = Value::undefined();
parameters[1] = Value(result.error());
// Pass the text, not the Error itself. valueToJSValue raises a
// ValueType::Error into the exception tracker instead of marshalling it as
// an argument, so the callback would never run and the promise behind it
// would stay pending. PersistentStoreModuleFactory does the same.
parameters[1] = Value(result.error().toString());
}

(*completion)(parameters.data(), parameters.size());
Expand Down
13 changes: 11 additions & 2 deletions src/valdi_modules/src/valdi/valdi_http/src/HTTPClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export class HTTPClient implements IHTTPClient {
body: ArrayBuffer | Uint8Array | undefined,
): CancelablePromise<HTTPResponse> {
let cancelFn: (() => void) | undefined;
let rejectFn: ((reason: unknown) => void) | undefined;
const promise = new Promise<HTTPResponse>((resolve, reject) => {
rejectFn = reject;
try {
const request: HTTPRequest = {
url: makeURL(this.baseUrl, pathOrUrl),
Expand All @@ -44,15 +46,22 @@ export class HTTPClient implements IHTTPClient {
if (response) {
resolve(response);
} else {
reject(error);
// A request manager reports its failure as text, so wrap it: callers should not have to
// ask whether the rejection they caught is a string or an Error.
reject(error instanceof Error ? error : new Error(String(error)));
}
});
} catch (err: unknown) {
reject(err);
}
});

return promiseToCancelablePromise(promise, () => cancelFn?.());
// No request manager reports a cancelled request, so without this the promise never settles.
// Rejecting an already settled one is a no-op, so a late cancel changes nothing.
return promiseToCancelablePromise(promise, () => {
cancelFn?.();
rejectFn?.(new Error('Request was cancelled'));
});
}

get(pathOrUrl: string, headers?: StringMap<string> | undefined): CancelablePromise<HTTPResponse> {
Expand Down
13 changes: 13 additions & 0 deletions tools/ci/test_standalone.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

# Tests for the standalone runtime whose deps come from this repo's MODULE.bazel.
set -eux

(

# Intended to be run from open_source/
cd "$(dirname "$0")/../.."

bzl test //valdi:test_standalone_http --test_output=errors

)
28 changes: 28 additions & 0 deletions valdi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,9 @@ valdi_test(
"//src/valdi_modules/src/cpp/valdi_http:valdi_http_cpp",
"//src/valdi_modules/src/valdi/persistence:persistence_native",
"//src/valdi_modules/src/valdi/valdi_core:valdi_core_native",
# The JavaScript side of valdi_http, for driving HTTPClient and asserting on the promise it
# hands back rather than only the native performRequest binding.
"//src/valdi_modules/src/valdi/valdi_http:valdi_http_native",
"//src/valdi_modules/src/valdi/valdi_protobuf:valdi_protobuf_native",
"//tsn",
"//valdi/testdata/resources/modules/local:local_native",
Expand Down Expand Up @@ -974,6 +977,30 @@ valdi_test(
],
)

cc_library(
name = "valdi_standalone_http",
srcs = glob(["src/valdi/standalone_http/**/*.cpp"]),
hdrs = glob(["src/valdi/standalone_http/**/*.hpp"]),
copts = CC_COMPILER_FLAGS,
strip_include_prefix = "src",
visibility = ["//visibility:public"],
deps = [
"//valdi_core:valdi_core_cc",
"@curl",
],
)

# Kept out of :test_integration and :test so those stay curl-free: Snap's internal build does not
# consume this repo's MODULE.bazel, so it cannot resolve @curl.
valdi_test(
name = "test_standalone_http",
srcs = glob(["test/standalone_http/**/*.cpp"]),
deps = [
":valdi_standalone_http",
"//valdi_core:valdi_core_cc",
],
)

valdi_test(
name = "test_hermes",
srcs = glob(["test/hermes/**/*.cpp"]),
Expand All @@ -994,6 +1021,7 @@ valdi_test(

cc_test(
name = "test",
timeout = "long",
linkstatic = 1,
visibility = ["//visibility:public"],
deps = [
Expand Down
6 changes: 5 additions & 1 deletion valdi/src/valdi/cli_runner/CLIRunner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

namespace Valdi {

int valdiCLIRun(const char* scriptPath, int argc, const char** argv) {
int valdiCLIRun(const char* scriptPath,
int argc,
const char** argv,
const std::shared_ptr<snap::valdi_core::HTTPRequestManager>& requestManager) {
SignalHandler::install();

StandaloneArguments standaloneArguments;
standaloneArguments.scriptPath = StringBox::fromCString(scriptPath);
standaloneArguments.enableHotReloader = false;
standaloneArguments.requestManager = requestManager;

if constexpr (snap::kIsDevBuild) {
standaloneArguments.enableDebuggerService = true;
Expand Down
15 changes: 14 additions & 1 deletion valdi/src/valdi/cli_runner/CLIRunner.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
#pragma once

#include <memory>

namespace snap::valdi_core {
class HTTPRequestManager;
}

namespace Valdi {

int valdiCLIRun(const char* scriptPath, int argc, const char** argv);
// requestManager is null for an application built without an HTTP backend. Not defaulted, so that
// each cli_main template says which one it is.
int valdiCLIRun(const char* scriptPath,
int argc,
const char** argv,
const std::shared_ptr<snap::valdi_core::HTTPRequestManager>& requestManager);

}
Loading
Loading