This is an automated email from the ASF dual-hosted git repository.
jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 8f1838cc025 Add airflow-ts-pack bundle build tool for TypeScript SDK
(#69295)
8f1838cc025 is described below
commit 8f1838cc025132f0f43ad23599b3966a06b18c8a
Author: Guan-Ming Chiu <[email protected]>
AuthorDate: Thu Jul 16 16:57:23 2026 +0900
Add airflow-ts-pack bundle build tool for TypeScript SDK (#69295)
* Add airflow-ts-pack bundle build tool for TypeScript SDK
* Embed TypeScript bundle metadata as leading comment
* Harden airflow-ts-pack manifest reading and packaging
Address review: strip entry shebangs so the metadata line stays first,
read the manifest via a sentinel line immune to import-time logging,
validate before writing bundle.mjs, make esbuild an optional peer
dependency, and reject null dags manifests.
* Validate embedded metadata size and split manifest out of runtime
---
.../airflow/sdk/coordinators/node/coordinator.py | 48 +++-
.../task_sdk/coordinators/node/test_coordinator.py | 86 ++++--
ts-sdk/README.md | 34 ++-
ts-sdk/example/README.md | 19 +-
ts-sdk/example/package.json | 2 +-
ts-sdk/package.json | 12 +
ts-sdk/pnpm-lock.yaml | 289 ++++++++++++++++++++-
ts-sdk/src/cli/main.ts | 28 ++
ts-sdk/src/cli/pack.ts | 225 ++++++++++++++++
ts-sdk/src/coordinator/manifest.ts | 44 ++++
ts-sdk/src/coordinator/runtime.ts | 9 +
ts-sdk/tests/cli/fixtures/empty-entry.ts | 22 ++
ts-sdk/tests/cli/fixtures/entry.ts | 26 ++
ts-sdk/tests/cli/fixtures/noisy-entry.ts | 27 ++
ts-sdk/tests/cli/pack.test.ts | 190 ++++++++++++++
ts-sdk/tests/coordinator/runtime-manifest.test.ts | 61 +++++
16 files changed, 1070 insertions(+), 52 deletions(-)
diff --git a/task-sdk/src/airflow/sdk/coordinators/node/coordinator.py
b/task-sdk/src/airflow/sdk/coordinators/node/coordinator.py
index 41b07710f8f..0d0d6b9151e 100644
--- a/task-sdk/src/airflow/sdk/coordinators/node/coordinator.py
+++ b/task-sdk/src/airflow/sdk/coordinators/node/coordinator.py
@@ -19,6 +19,7 @@
from __future__ import annotations
+import base64
import os
import pathlib
from typing import TYPE_CHECKING, Any
@@ -45,6 +46,38 @@ log: FilteringBoundLogger =
structlog.get_logger(logger_name="coordinators.node"
BUNDLE_FILENAME = "bundle.mjs"
METADATA_FILENAME = "airflow-metadata.yaml"
+EMBEDDED_METADATA_MARKER = b"//# airflowMetadata="
+EMBEDDED_METADATA_MAX_BYTES = 1024 * 1024
+
+
+def _read_embedded_metadata(bundle_path: pathlib.Path) -> dict[str, Any] |
None:
+ """
+ Read the manifest ``airflow-ts-pack`` embeds in the bundle itself.
+
+ The packer prepends the ``airflow-metadata.yaml`` content as a leading
+ ``//# airflowMetadata=<base64>`` line comment, keeping bundle and metadata
+ a single artifact. Returns ``None`` when the bundle has no such marker.
+ """
+ try:
+ with bundle_path.open("rb") as bundle_file:
+ line = bundle_file.readline(EMBEDDED_METADATA_MAX_BYTES + 1)
+ except OSError as exc:
+ raise ValueError(f"cannot read {bundle_path.name}: {exc}") from exc
+
+ if not line.startswith(EMBEDDED_METADATA_MARKER):
+ return None
+ if len(line) > EMBEDDED_METADATA_MAX_BYTES:
+ raise ValueError(
+ f"embedded airflow metadata exceeds {EMBEDDED_METADATA_MAX_BYTES}
bytes; "
+ f"rebuild {bundle_path.name} with airflow-ts-pack"
+ )
+
+ payload = line[len(EMBEDDED_METADATA_MARKER) :].strip()
+ try:
+ decoded = base64.b64decode(payload, validate=True)
+ except ValueError as exc:
+ raise ValueError(f"cannot parse embedded airflow metadata: {exc}")
from exc
+ return parse_metadata_mapping(decoded, source="embedded airflow metadata")
def _read_bundle_metadata(metadata_path: pathlib.Path) -> dict[str, Any]:
@@ -63,8 +96,9 @@ def _find_bundle(bundles_root: Sequence[pathlib.Path]) ->
ResolvedBundle:
"""
Locate the ``.mjs`` entry point in *bundles_root*.
- Scans each configured directory for ``bundle.mjs`` and reads the sibling
- ``airflow-metadata.yaml`` for the bundle's supervisor schema version.
+ Scans each configured directory for ``bundle.mjs`` and reads the bundle's
+ supervisor schema version from the metadata embedded in the bundle,
+ falling back to a sibling ``airflow-metadata.yaml`` sidecar.
This is an ordered fallback search, not Dag/task-aware multi-bundle
routing. The first bundle found wins. A future version can use the
@@ -77,7 +111,9 @@ def _find_bundle(bundles_root: Sequence[pathlib.Path]) ->
ResolvedBundle:
if not candidate.is_file():
continue
try:
- metadata = _read_bundle_metadata(root / METADATA_FILENAME)
+ metadata = _read_embedded_metadata(candidate)
+ if metadata is None:
+ metadata = _read_bundle_metadata(root / METADATA_FILENAME)
log.debug("Selected TypeScript bundle", path=candidate, root=root)
return ResolvedBundle(
path=candidate,
@@ -123,8 +159,10 @@ class NodeCoordinator(SubprocessCoordinator):
``"node"``, which relies on ``$PATH``).
:param bundles_root: Ordered list of directories scanned for a usable
TypeScript bundle. Each bundle directory must contain ``bundle.mjs``
- and ``airflow-metadata.yaml``. This is a fallback search path; it does
- not yet route different Dag/task pairs to different bundles.
+ with embedded metadata (as produced by ``airflow-ts-pack``), or
+ ``bundle.mjs`` plus an ``airflow-metadata.yaml`` sidecar. This is a
+ fallback search path; it does not yet route different Dag/task pairs
+ to different bundles.
:param task_startup_timeout: Maximum time the coordinator waits for a task
process to start, in seconds. The default is 10 seconds.
"""
diff --git a/task-sdk/tests/task_sdk/coordinators/node/test_coordinator.py
b/task-sdk/tests/task_sdk/coordinators/node/test_coordinator.py
index 3538c389a19..5222723953d 100644
--- a/task-sdk/tests/task_sdk/coordinators/node/test_coordinator.py
+++ b/task-sdk/tests/task_sdk/coordinators/node/test_coordinator.py
@@ -18,6 +18,7 @@
from __future__ import annotations
+import base64
import pathlib
import pytest
@@ -25,6 +26,7 @@ from uuid6 import uuid7
from airflow.sdk.api.datamodels._generated import TaskInstance
from airflow.sdk.coordinators.node.coordinator import (
+ EMBEDDED_METADATA_MAX_BYTES,
NodeCoordinator,
_find_bundle,
)
@@ -45,27 +47,33 @@ def _make_ti(dag_id: str = "test_dag", queue: str = "ts")
-> TaskInstance:
)
+def _metadata_yaml(schema_version: str) -> str:
+ return f"""\
+airflow_bundle_metadata_version: "1.0"
+sdk:
+ language: typescript
+ version: "0.1.0"
+ supervisor_schema_version: "{schema_version}"
+source: src/airflow.ts
+dags:
+ test_dag:
+ tasks:
+ - test_task
+"""
+
+
def write_bundle(root: pathlib.Path, schema_version: str = SCHEMA_VERSION) ->
pathlib.Path:
bundle = root / "bundle.mjs"
bundle.write_text("export {};\n", encoding="utf-8")
- (root / "airflow-metadata.yaml").write_text(
- "\n".join(
- [
- 'airflow_bundle_metadata_version: "1.0"',
- "sdk:",
- " language: typescript",
- ' version: "0.1.0"',
- f' supervisor_schema_version: "{schema_version}"',
- "source: src/airflow.ts",
- "dags:",
- " test_dag:",
- " tasks:",
- " - test_task",
- "",
- ]
- ),
- encoding="utf-8",
- )
+ (root /
"airflow-metadata.yaml").write_text(_metadata_yaml(schema_version),
encoding="utf-8")
+ return bundle
+
+
+def write_embedded_bundle(root: pathlib.Path, payload: str | None = None) ->
pathlib.Path:
+ if payload is None:
+ payload =
base64.b64encode(_metadata_yaml(SCHEMA_VERSION).encode("utf-8")).decode("ascii")
+ bundle = root / "bundle.mjs"
+ bundle.write_text(f"//# airflowMetadata={payload}\nexport {{}};\n",
encoding="utf-8")
return bundle
@@ -105,6 +113,48 @@ class TestNodeCoordinatorBundleSelection:
assert found.path == bundle
assert found.schema_version == SCHEMA_VERSION
+ def test_find_bundle_reads_embedded_metadata_without_sidecar(self,
tmp_path):
+ bundle = write_embedded_bundle(tmp_path)
+
+ found = _find_bundle([tmp_path])
+
+ assert found.path == bundle
+ assert found.schema_version == SCHEMA_VERSION
+
+ def test_find_bundle_prefers_embedded_metadata_over_sidecar(self,
tmp_path):
+ bundle = write_embedded_bundle(tmp_path)
+ (tmp_path / "airflow-metadata.yaml").write_text("[not-a-mapping]\n",
encoding="utf-8")
+
+ found = _find_bundle([tmp_path])
+
+ assert found.path == bundle
+ assert found.schema_version == SCHEMA_VERSION
+
+ @pytest.mark.parametrize(
+ ("payload", "message"),
+ [
+ ("not-base64!", "cannot parse embedded airflow metadata"),
+ (base64.b64encode(b"[not-a-mapping]").decode("ascii"), "must
contain a mapping"),
+ ],
+ )
+ def test_find_bundle_rejects_invalid_embedded_metadata(self, tmp_path,
payload, message):
+ write_embedded_bundle(tmp_path, payload=payload)
+
+ with pytest.raises(FileNotFoundError, match=message):
+ _find_bundle([tmp_path])
+
+ def test_find_bundle_rejects_oversized_embedded_metadata(self, tmp_path):
+ write_embedded_bundle(tmp_path, payload="A" *
EMBEDDED_METADATA_MAX_BYTES)
+
+ with pytest.raises(FileNotFoundError, match="embedded airflow metadata
exceeds"):
+ _find_bundle([tmp_path])
+
+ def test_find_bundle_rejects_empty_marker(self, tmp_path):
+ (tmp_path / "bundle.mjs").write_text("//# airflowMetadata=\nexport
{};\n", encoding="utf-8")
+
+ with pytest.raises(FileNotFoundError, match="must contain a mapping"):
+ _find_bundle([tmp_path])
+
def test_find_bundle_checks_multiple_roots(self, tmp_path):
first = tmp_path / "first"
second = tmp_path / "second"
diff --git a/ts-sdk/README.md b/ts-sdk/README.md
index 3ac7360aca6..15d17efd937 100644
--- a/ts-sdk/README.md
+++ b/ts-sdk/README.md
@@ -90,8 +90,10 @@ coordinators = {
queue_to_coordinator = {"typescript": "ts"}
```
-Each configured bundle directory must contain `bundle.mjs` and
-`airflow-metadata.yaml`.
+Each configured bundle directory must contain a `bundle.mjs` built with
+`airflow-ts-pack` (see [Packing bundles](#packing-bundles)), which embeds the
+Airflow metadata in the bundle itself. A `bundle.mjs` without embedded
+metadata is also accepted alongside an `airflow-metadata.yaml` sidecar.
TypeScript entrypoint:
@@ -147,8 +149,32 @@ Airflow launches the bundled entrypoint with
`--comm=host:port` and
the task startup message, finds the registered handler for the Dag/task pair,
and reports the terminal task state back to Airflow.
-See [`example/`](example/) for a coordinator-runtime example that builds a
-`bundle.mjs` with `esbuild` and uses a Python stub Dag.
+See [`example/`](example/) for a coordinator-runtime example that packs a
+bundle with `airflow-ts-pack` and uses a Python stub Dag.
+
+## Packing bundles
+
+`airflow-ts-pack` produces everything `NodeCoordinator` needs in one command.
+Packing is build-time only, so `esbuild` is an optional peer dependency the
+runtime install skips:
+
+```bash
+npm install --save-dev esbuild
+airflow-ts-pack src/main.ts --outdir dist
+```
+
+It bundles the entrypoint into `dist/bundle.mjs` with esbuild, runs the
+bundle with `--airflow-metadata` so the bundle reports its own registered
+Dag/task pairs and supervisor schema version, and embeds that manifest in the
+bundle as a leading `//# airflowMetadata=<base64>` comment. The result is a
+single deployable file whose metadata cannot drift from its code; no
+hand-written sidecar is needed.
+
+Options:
+
+- `--outdir <dir>` — output directory (default `dist`)
+- `--source <name>` — display name of the primary source file shown in the
+ Airflow UI (default: entry basename)
## TaskClient
diff --git a/ts-sdk/example/README.md b/ts-sdk/example/README.md
index 181135a5231..1ea267bdedf 100644
--- a/ts-sdk/example/README.md
+++ b/ts-sdk/example/README.md
@@ -26,8 +26,9 @@ This example shows the coordinator-mode shape for TypeScript
task handlers:
starts the coordinator runtime.
- `dist/bundle.mjs` is the generated Node.js bundle that Airflow launches.
-The TypeScript SDK does not include a packer yet, so this example builds the
-bundle with `esbuild` and writes the Airflow metadata file manually.
+The build uses the SDK's `airflow-ts-pack` tool, which bundles the entrypoint
+with esbuild and embeds the Airflow metadata generated from the bundle's
+registered tasks, producing a single deployable file.
## Build
@@ -39,7 +40,7 @@ pnpm install
pnpm run build
```
-Build the example bundle:
+Build the example bundle and its metadata:
```bash
cd ts-sdk/example
@@ -47,23 +48,11 @@ pnpm install
pnpm run build
```
-Create the metadata file next to the generated bundle:
-
-```bash
-node --input-type=module > dist/airflow-metadata.yaml <<'EOF'
-import { SUPERVISOR_API_VERSION } from "@apache-airflow/ts-sdk";
-
-console.log(`sdk:
- supervisor_schema_version: "${SUPERVISOR_API_VERSION}"`);
-EOF
-```
-
The coordinator expects this layout:
```text
ts-sdk/example/dist/
bundle.mjs
- airflow-metadata.yaml
```
## Airflow Configuration
diff --git a/ts-sdk/example/package.json b/ts-sdk/example/package.json
index 5594727113c..618747ca523 100644
--- a/ts-sdk/example/package.json
+++ b/ts-sdk/example/package.json
@@ -5,7 +5,7 @@
"type": "module",
"license": "Apache-2.0",
"scripts": {
- "build": "esbuild src/main.ts --bundle --platform=node --format=esm
--target=node22 --outfile=dist/bundle.mjs",
+ "build": "airflow-ts-pack src/main.ts --outdir dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
diff --git a/ts-sdk/package.json b/ts-sdk/package.json
index 190df171014..0599e165524 100644
--- a/ts-sdk/package.json
+++ b/ts-sdk/package.json
@@ -7,6 +7,9 @@
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+ "bin": {
+ "airflow-ts-pack": "./dist/cli/main.js"
+ },
"homepage": "https://airflow.apache.org",
"repository": {
"type": "git",
@@ -59,8 +62,17 @@
"dependencies": {
"@msgpack/msgpack": "^3.1.2"
},
+ "peerDependencies": {
+ "esbuild": "^0.28.1"
+ },
+ "peerDependenciesMeta": {
+ "esbuild": {
+ "optional": true
+ }
+ },
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "esbuild": "^0.28.1",
"@types/node": "^22.19.17",
"eslint": "^10.4.0",
"json-schema-to-typescript": "^15.0.4",
diff --git a/ts-sdk/pnpm-lock.yaml b/ts-sdk/pnpm-lock.yaml
index 887acdcc7e4..1689afb0eba 100644
--- a/ts-sdk/pnpm-lock.yaml
+++ b/ts-sdk/pnpm-lock.yaml
@@ -18,6 +18,9 @@ importers:
'@types/node':
specifier: ^22.19.17
version: 22.19.17
+ esbuild:
+ specifier: ^0.28.1
+ version: 0.28.1
eslint:
specifier: ^10.4.0
version: 10.4.0
@@ -38,7 +41,7 @@ importers:
version: 8.60.0([email protected])([email protected])
vitest:
specifier: ^4.1.7
- version:
4.1.7(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected]))
+ version:
4.1.7(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected]))
packages:
@@ -82,156 +85,312 @@ packages:
cpu: [ppc64]
os: [aix]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
'@esbuild/[email protected]':
resolution: {integrity:
sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
+ '@esbuild/[email protected]':
+ resolution: {integrity:
sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@eslint-community/[email protected]':
resolution: {integrity:
sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -599,6 +758,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ [email protected]:
+ resolution: {integrity:
sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
[email protected]:
resolution: {integrity:
sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -1160,81 +1324,159 @@ snapshots:
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@esbuild/[email protected]':
optional: true
+ '@esbuild/[email protected]':
+ optional: true
+
'@eslint-community/[email protected]([email protected])':
dependencies:
eslint: 10.4.0
@@ -1489,7 +1731,7 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
- vitest:
4.1.7(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected]))
+ vitest:
4.1.7(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected]))
optional: true
'@vitest/[email protected]':
@@ -1501,13 +1743,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
-
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected]))':
+
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected]))':
dependencies:
'@vitest/spy': 4.1.7
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.0.10(@types/[email protected])([email protected])([email protected])
+ vite: 8.0.10(@types/[email protected])([email protected])([email protected])
'@vitest/[email protected]':
dependencies:
@@ -1612,6 +1854,35 @@ snapshots:
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
+ [email protected]:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.1
+ '@esbuild/android-arm': 0.28.1
+ '@esbuild/android-arm64': 0.28.1
+ '@esbuild/android-x64': 0.28.1
+ '@esbuild/darwin-arm64': 0.28.1
+ '@esbuild/darwin-x64': 0.28.1
+ '@esbuild/freebsd-arm64': 0.28.1
+ '@esbuild/freebsd-x64': 0.28.1
+ '@esbuild/linux-arm': 0.28.1
+ '@esbuild/linux-arm64': 0.28.1
+ '@esbuild/linux-ia32': 0.28.1
+ '@esbuild/linux-loong64': 0.28.1
+ '@esbuild/linux-mips64el': 0.28.1
+ '@esbuild/linux-ppc64': 0.28.1
+ '@esbuild/linux-riscv64': 0.28.1
+ '@esbuild/linux-s390x': 0.28.1
+ '@esbuild/linux-x64': 0.28.1
+ '@esbuild/netbsd-arm64': 0.28.1
+ '@esbuild/netbsd-x64': 0.28.1
+ '@esbuild/openbsd-arm64': 0.28.1
+ '@esbuild/openbsd-x64': 0.28.1
+ '@esbuild/openharmony-arm64': 0.28.1
+ '@esbuild/sunos-x64': 0.28.1
+ '@esbuild/win32-arm64': 0.28.1
+ '@esbuild/win32-ia32': 0.28.1
+ '@esbuild/win32-x64': 0.28.1
+
[email protected]: {}
[email protected]:
@@ -2007,7 +2278,7 @@ snapshots:
dependencies:
punycode: 2.3.1
- [email protected](@types/[email protected])([email protected])([email protected]):
+ [email protected](@types/[email protected])([email protected])([email protected]):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -2016,14 +2287,14 @@ snapshots:
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 22.19.17
- esbuild: 0.27.7
+ esbuild: 0.28.1
fsevents: 2.3.3
tsx: 4.21.0
-
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])):
+
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])):
dependencies:
'@vitest/expect': 4.1.7
- '@vitest/mocker':
4.1.7([email protected](@types/[email protected])([email protected])([email protected]))
+ '@vitest/mocker':
4.1.7([email protected](@types/[email protected])([email protected])([email protected]))
'@vitest/pretty-format': 4.1.7
'@vitest/runner': 4.1.7
'@vitest/snapshot': 4.1.7
@@ -2040,7 +2311,7 @@ snapshots:
tinyexec: 1.1.1
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
- vite: 8.0.10(@types/[email protected])([email protected])([email protected])
+ vite: 8.0.10(@types/[email protected])([email protected])([email protected])
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.17
diff --git a/ts-sdk/src/cli/main.ts b/ts-sdk/src/cli/main.ts
new file mode 100644
index 00000000000..1db958183fe
--- /dev/null
+++ b/ts-sdk/src/cli/main.ts
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { runPack } from "./pack.js";
+
+try {
+ await runPack(process.argv.slice(2));
+} catch (error) {
+ console.error(error instanceof Error ? error.message : error);
+ process.exit(1);
+}
diff --git a/ts-sdk/src/cli/pack.ts b/ts-sdk/src/cli/pack.ts
new file mode 100644
index 00000000000..0d863fbe898
--- /dev/null
+++ b/ts-sdk/src/cli/pack.ts
@@ -0,0 +1,225 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// airflow-ts-pack: bundle a TypeScript entrypoint into the single-file
+// artifact NodeCoordinator consumes — `bundle.mjs` with the airflow
+// metadata embedded as a leading `//# airflowMetadata=<base64>` comment.
+//
+// Mirrors airflow-go-pack: build first, then run the built bundle with
+// --airflow-metadata so the manifest comes from the bundle's own task
+// registry and schema version, never from a hand-written sidecar.
+
+import { execFileSync } from "node:child_process";
+import { readFileSync, rmSync, writeFileSync } from "node:fs";
+import path from "node:path";
+
+import {
+ AIRFLOW_METADATA_FLAG,
+ AIRFLOW_METADATA_SENTINEL,
+ type BundleManifest,
+} from "../coordinator/manifest.js";
+
+const AIRFLOW_BUNDLE_METADATA_VERSION = "1.0";
+const BUNDLE_FILENAME = "bundle.mjs";
+// bundle.mjs is written only after validation, so failures leave no partial
artifact.
+const STAGING_FILENAME = "bundle.pack-staging.mjs";
+const MANIFEST_TIMEOUT_MS = 60_000;
+const MANIFEST_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
+const EMBEDDED_METADATA_MAX_BYTES = 1024 * 1024;
+export const EMBEDDED_METADATA_PREFIX = "//# airflowMetadata=";
+
+const USAGE = `Usage: airflow-ts-pack <entry> [--outdir <dir>] [--source
<name>]
+
+Bundles <entry> into <outdir>/${BUNDLE_FILENAME} with esbuild and embeds the
+airflow metadata generated from the bundle's registered tasks.
+
+Options:
+ --outdir <dir> Output directory (default: dist)
+ --source <name> Display name of the primary source file (default: <entry>
basename)
+`;
+
+export interface PackArgs {
+ entry: string;
+ outdir: string;
+ source: string;
+}
+
+function usageError(message: string): Error {
+ return new Error(`${message}\n\n${USAGE}`);
+}
+
+export function parsePackArgs(argv: readonly string[]): PackArgs {
+ let entry: string | null = null;
+ let outdir = "dist";
+ let source: string | null = null;
+ for (let i = 0; i < argv.length; i += 1) {
+ const arg = argv[i]!;
+ if (arg === "--outdir" || arg === "--source") {
+ const value = argv[i + 1];
+ if (!value) throw usageError(`${arg} requires a value`);
+ if (arg === "--outdir") outdir = value;
+ else source = value;
+ i += 1;
+ } else if (arg.startsWith("-")) {
+ throw usageError(`Unknown option ${arg}`);
+ } else if (entry) {
+ throw usageError(`Unexpected argument ${arg}`);
+ } else {
+ entry = arg;
+ }
+ }
+ if (!entry) throw usageError("Missing entry file");
+ return { entry, outdir, source: source ?? path.basename(entry) };
+}
+
+export interface PackMetadata {
+ airflow_bundle_metadata_version: string;
+ sdk: { language: string; version: string; supervisor_schema_version: string
};
+ source: string;
+ dags: BundleManifest["dags"];
+}
+
+// JSON string literals are valid YAML double-quoted scalars, so every
+// scalar below is emitted through JSON.stringify for correct escaping.
+export function renderMetadataYaml(metadata: PackMetadata): string {
+ const lines = [
+ `airflow_bundle_metadata_version:
${JSON.stringify(metadata.airflow_bundle_metadata_version)}`,
+ "sdk:",
+ ` language: ${JSON.stringify(metadata.sdk.language)}`,
+ ` version: ${JSON.stringify(metadata.sdk.version)}`,
+ ` supervisor_schema_version:
${JSON.stringify(metadata.sdk.supervisor_schema_version)}`,
+ `source: ${JSON.stringify(metadata.source)}`,
+ "dags:",
+ ];
+ for (const [dagId, dag] of Object.entries(metadata.dags)) {
+ lines.push(` ${JSON.stringify(dagId)}:`);
+ lines.push(` tasks: [${dag.tasks.map((task) =>
JSON.stringify(task)).join(", ")}]`);
+ }
+ return `${lines.join("\n")}\n`;
+}
+
+function readSdkVersion(): string {
+ const packageJsonUrl = new URL("../../package.json", import.meta.url);
+ const { version } = JSON.parse(readFileSync(packageJsonUrl, "utf-8")) as {
version: string };
+ return version;
+}
+
+function readBundleManifest(bundlePath: string): BundleManifest {
+ let stdout: string;
+ try {
+ stdout = execFileSync(process.execPath, [bundlePath,
AIRFLOW_METADATA_FLAG], {
+ encoding: "utf-8",
+ timeout: MANIFEST_TIMEOUT_MS,
+ maxBuffer: MANIFEST_MAX_BUFFER_BYTES,
+ });
+ } catch (error) {
+ throw new Error(`Running the bundle with ${AIRFLOW_METADATA_FLAG} failed:
${String(error)}`, {
+ cause: error,
+ });
+ }
+
+ // Import-time logging from user code lands on stdout too; pick the sentinel
line.
+ const line = stdout
+ .split("\n")
+ .reverse()
+ .find((candidate) => candidate.startsWith(AIRFLOW_METADATA_SENTINEL));
+ if (line === undefined) {
+ throw new Error(`Bundle produced no ${AIRFLOW_METADATA_FLAG} output`);
+ }
+
+ let manifest: BundleManifest;
+ try {
+ manifest = JSON.parse(line.slice(AIRFLOW_METADATA_SENTINEL.length)) as
BundleManifest;
+ } catch (error) {
+ throw new Error(`Bundle produced invalid ${AIRFLOW_METADATA_FLAG} output:
${String(error)}`, {
+ cause: error,
+ });
+ }
+ if (!manifest.supervisor_schema_version || !manifest.dags || typeof
manifest.dags !== "object") {
+ throw new Error(`Bundle produced incomplete ${AIRFLOW_METADATA_FLAG}
output`);
+ }
+ return manifest;
+}
+
+// esbuild keeps an entry hashbang as line 1, where the metadata comment must
go;
+// NodeCoordinator always runs the bundle through `node`, so drop it.
+function stripShebang(bundle: string): string {
+ if (!bundle.startsWith("#!")) return bundle;
+ const newline = bundle.indexOf("\n");
+ return newline === -1 ? "" : bundle.slice(newline + 1);
+}
+
+async function loadEsbuild(): Promise<typeof import("esbuild")> {
+ try {
+ return await import("esbuild");
+ } catch (error) {
+ throw new Error(
+ "airflow-ts-pack needs esbuild; install it alongside the SDK (e.g. `npm
i -D esbuild`)",
+ { cause: error },
+ );
+ }
+}
+
+export async function runPack(argv: readonly string[]): Promise<void> {
+ const args = parsePackArgs(argv);
+ const bundlePath = path.join(args.outdir, BUNDLE_FILENAME);
+ const stagingPath = path.join(args.outdir, STAGING_FILENAME);
+ const { build } = await loadEsbuild();
+
+ try {
+ await build({
+ entryPoints: [args.entry],
+ bundle: true,
+ platform: "node",
+ format: "esm",
+ target: "node22",
+ outfile: stagingPath,
+ });
+
+ const manifest = readBundleManifest(stagingPath);
+ if (Object.keys(manifest.dags).length === 0) {
+ throw new Error(
+ `${args.entry} registered no tasks; call registerTask(...) before
startCoordinator()`,
+ );
+ }
+
+ const metadataYaml = renderMetadataYaml({
+ airflow_bundle_metadata_version: AIRFLOW_BUNDLE_METADATA_VERSION,
+ sdk: {
+ language: "typescript",
+ version: readSdkVersion(),
+ supervisor_schema_version: manifest.supervisor_schema_version,
+ },
+ source: args.source,
+ dags: manifest.dags,
+ });
+ const metadataLine =
`${EMBEDDED_METADATA_PREFIX}${Buffer.from(metadataYaml,
"utf-8").toString("base64")}\n`;
+ if (metadataLine.length > EMBEDDED_METADATA_MAX_BYTES) {
+ throw new Error(
+ `Embedded airflow metadata is ${metadataLine.length} bytes, ` +
+ `over the ${EMBEDDED_METADATA_MAX_BYTES} byte limit; reduce the
number of registered tasks`,
+ );
+ }
+ writeFileSync(bundlePath, metadataLine +
stripShebang(readFileSync(stagingPath, "utf-8")));
+ } finally {
+ rmSync(stagingPath, { force: true });
+ }
+
+ console.log(`Wrote ${bundlePath} (airflow metadata embedded)`);
+}
diff --git a/ts-sdk/src/coordinator/manifest.ts
b/ts-sdk/src/coordinator/manifest.ts
new file mode 100644
index 00000000000..32e503dce7a
--- /dev/null
+++ b/ts-sdk/src/coordinator/manifest.ts
@@ -0,0 +1,44 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SUPERVISOR_API_VERSION } from "./protocol.js";
+import { listRegisteredTasks, type TaskRegistration } from
"../sdk/registry.js";
+
+export const AIRFLOW_METADATA_FLAG = "--airflow-metadata";
+
+/** Marks the manifest line on stdout, which import-time logging may also
reach. */
+export const AIRFLOW_METADATA_SENTINEL = "__AIRFLOW_METADATA__ ";
+
+/** Bundle manifest fields only the built bundle itself knows: the schema
+ * version it was compiled against and the Dag/task pairs it registered.
+ * `airflow-ts-pack` runs `node bundle.mjs --airflow-metadata` to read this.
*/
+export interface BundleManifest {
+ supervisor_schema_version: string;
+ dags: Record<string, { tasks: string[] }>;
+}
+
+export function buildBundleManifest(
+ registrations: readonly TaskRegistration[] = listRegisteredTasks(),
+): BundleManifest {
+ const dags: BundleManifest["dags"] = {};
+ for (const { dagId, taskId } of registrations) {
+ (dags[dagId] ??= { tasks: [] }).tasks.push(taskId);
+ }
+ return { supervisor_schema_version: SUPERVISOR_API_VERSION, dags };
+}
diff --git a/ts-sdk/src/coordinator/runtime.ts
b/ts-sdk/src/coordinator/runtime.ts
index d083bf4acce..5626aae3106 100644
--- a/ts-sdk/src/coordinator/runtime.ts
+++ b/ts-sdk/src/coordinator/runtime.ts
@@ -37,6 +37,11 @@
import { createCoordinatorClient } from "./client.js";
import { CommChannel } from "./comm-channel.js";
import { LogChannel } from "./log-channel.js";
+import {
+ AIRFLOW_METADATA_FLAG,
+ AIRFLOW_METADATA_SENTINEL,
+ buildBundleManifest,
+} from "./manifest.js";
import {
asMsgFromSupervisor,
SUPERVISOR_API_VERSION,
@@ -107,6 +112,10 @@ export function parseArgs(argv: readonly string[]):
ParsedArgs {
* delivered its terminal frame and closed both sockets. */
export async function startCoordinator(opts: StartCoordinatorOptions = {}):
Promise<void> {
const argv = opts.argv ?? process.argv;
+ if (argv.includes(AIRFLOW_METADATA_FLAG)) {
+
process.stdout.write(`${AIRFLOW_METADATA_SENTINEL}${JSON.stringify(buildBundleManifest())}\n`);
+ return;
+ }
const parsed =
opts.commAddr && opts.logsAddr
? { commAddr: opts.commAddr, logsAddr: opts.logsAddr }
diff --git a/ts-sdk/tests/cli/fixtures/empty-entry.ts
b/ts-sdk/tests/cli/fixtures/empty-entry.ts
new file mode 100644
index 00000000000..3e2b582b224
--- /dev/null
+++ b/ts-sdk/tests/cli/fixtures/empty-entry.ts
@@ -0,0 +1,22 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { startCoordinator } from "../../../src/index.js";
+
+await startCoordinator();
diff --git a/ts-sdk/tests/cli/fixtures/entry.ts
b/ts-sdk/tests/cli/fixtures/entry.ts
new file mode 100644
index 00000000000..8d53c43aee5
--- /dev/null
+++ b/ts-sdk/tests/cli/fixtures/entry.ts
@@ -0,0 +1,26 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { registerTask, startCoordinator } from "../../../src/index.js";
+
+registerTask({ dagId: "fixture_dag", taskId: "extract" }, async () =>
"extracted");
+registerTask({ dagId: "fixture_dag", taskId: "transform" }, async () =>
"transformed");
+registerTask({ dagId: "other_dag", taskId: "solo" }, async () => undefined);
+
+await startCoordinator();
diff --git a/ts-sdk/tests/cli/fixtures/noisy-entry.ts
b/ts-sdk/tests/cli/fixtures/noisy-entry.ts
new file mode 100644
index 00000000000..3b9675619d3
--- /dev/null
+++ b/ts-sdk/tests/cli/fixtures/noisy-entry.ts
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { registerTask, startCoordinator } from "../../../src/index.js";
+
+console.log("noise from an import-time dependency");
+
+registerTask({ dagId: "noisy_dag", taskId: "only" }, async () => undefined);
+
+await startCoordinator();
diff --git a/ts-sdk/tests/cli/pack.test.ts b/ts-sdk/tests/cli/pack.test.ts
new file mode 100644
index 00000000000..4a5c2d572bf
--- /dev/null
+++ b/ts-sdk/tests/cli/pack.test.ts
@@ -0,0 +1,190 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { execFileSync } from "node:child_process";
+import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from
"node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { afterEach, describe, expect, it } from "vitest";
+
+import {
+ EMBEDDED_METADATA_PREFIX,
+ parsePackArgs,
+ renderMetadataYaml,
+ runPack,
+} from "../../src/cli/pack.js";
+import { SUPERVISOR_API_VERSION } from "../../src/coordinator/protocol.js";
+import { AIRFLOW_METADATA_SENTINEL } from "../../src/coordinator/manifest.js";
+
+const FIXTURE_ENTRY = fileURLToPath(new URL("fixtures/entry.ts",
import.meta.url));
+const NOISY_ENTRY = fileURLToPath(new URL("fixtures/noisy-entry.ts",
import.meta.url));
+const EMPTY_ENTRY = fileURLToPath(new URL("fixtures/empty-entry.ts",
import.meta.url));
+const SDK_INDEX = fileURLToPath(new URL("../../src/index.ts",
import.meta.url));
+const SDK_VERSION = (
+ JSON.parse(readFileSync(new URL("../../package.json", import.meta.url),
"utf-8")) as {
+ version: string;
+ }
+).version;
+
+describe("parsePackArgs", () => {
+ it("parses entry with defaults", () => {
+ expect(parsePackArgs(["src/main.ts"])).toEqual({
+ entry: "src/main.ts",
+ outdir: "dist",
+ source: "main.ts",
+ });
+ });
+
+ it("parses --outdir and --source overrides", () => {
+ expect(parsePackArgs(["src/main.ts", "--outdir", "build", "--source",
"pipeline.ts"])).toEqual({
+ entry: "src/main.ts",
+ outdir: "build",
+ source: "pipeline.ts",
+ });
+ });
+
+ it.each([
+ [[], "Missing entry file"],
+ [["--outdir"], "--outdir requires a value"],
+ [["a.ts", "b.ts"], "Unexpected argument b.ts"],
+ [["a.ts", "--bogus"], "Unknown option --bogus"],
+ ])("rejects %j", (argv, message) => {
+ expect(() => parsePackArgs(argv)).toThrow(message);
+ });
+});
+
+describe("renderMetadataYaml", () => {
+ it("emits schema-conformant YAML with escaped scalars", () => {
+ const yaml = renderMetadataYaml({
+ airflow_bundle_metadata_version: "1.0",
+ sdk: { language: "typescript", version: "0.1.0",
supervisor_schema_version: "2026-06-16" },
+ source: 'we"ird.ts',
+ dags: { my_dag: { tasks: ["a", 'b"c'] } },
+ });
+ expect(yaml).toBe(
+ [
+ 'airflow_bundle_metadata_version: "1.0"',
+ "sdk:",
+ ' language: "typescript"',
+ ' version: "0.1.0"',
+ ' supervisor_schema_version: "2026-06-16"',
+ 'source: "we\\"ird.ts"',
+ "dags:",
+ ' "my_dag":',
+ ' tasks: ["a", "b\\"c"]',
+ "",
+ ].join("\n"),
+ );
+ });
+});
+
+describe("runPack", () => {
+ let outdir: string;
+
+ afterEach(() => {
+ if (outdir) rmSync(outdir, { recursive: true, force: true });
+ });
+
+ it("bundles the entry and embeds metadata from the bundle's registry", async
() => {
+ outdir = mkdtempSync(path.join(tmpdir(), "ts-pack-"));
+ const nested = path.join(outdir, "dist");
+ await runPack([FIXTURE_ENTRY, "--outdir", nested]);
+
+ const bundlePath = path.join(nested, "bundle.mjs");
+ expect(existsSync(path.join(nested, "airflow-metadata.yaml"))).toBe(false);
+
+ const firstLine = readFileSync(bundlePath, "utf-8").split("\n")[0]!;
+ expect(firstLine.startsWith(EMBEDDED_METADATA_PREFIX)).toBe(true);
+ const metadata = Buffer.from(
+ firstLine.slice(EMBEDDED_METADATA_PREFIX.length),
+ "base64",
+ ).toString("utf-8");
+ expect(metadata).toBe(
+ [
+ 'airflow_bundle_metadata_version: "1.0"',
+ "sdk:",
+ ' language: "typescript"',
+ ` version: ${JSON.stringify(SDK_VERSION)}`,
+ ` supervisor_schema_version:
${JSON.stringify(SUPERVISOR_API_VERSION)}`,
+ 'source: "entry.ts"',
+ "dags:",
+ ' "fixture_dag":',
+ ' tasks: ["extract", "transform"]',
+ ' "other_dag":',
+ ' tasks: ["solo"]',
+ "",
+ ].join("\n"),
+ );
+
+ const dumped = execFileSync(process.execPath, [bundlePath,
"--airflow-metadata"], {
+ encoding: "utf-8",
+ });
+ expect(dumped.startsWith(AIRFLOW_METADATA_SENTINEL)).toBe(true);
+ expect(
+
JSON.parse(dumped.slice(AIRFLOW_METADATA_SENTINEL.length)).supervisor_schema_version,
+ ).toBe(SUPERVISOR_API_VERSION);
+ });
+
+ it("keeps a shebang entry runnable and reads the manifest past import-time
logging", async () => {
+ outdir = mkdtempSync(path.join(tmpdir(), "ts-pack-"));
+ await runPack([NOISY_ENTRY, "--outdir", outdir]);
+
+ const bundlePath = path.join(outdir, "bundle.mjs");
+ const bundle = readFileSync(bundlePath, "utf-8");
+ expect(bundle.startsWith(EMBEDDED_METADATA_PREFIX)).toBe(true);
+ expect(bundle).not.toContain("#!/usr/bin/env node");
+ expect(existsSync(path.join(outdir,
"bundle.pack-staging.mjs"))).toBe(false);
+
+ const metadata = Buffer.from(
+ bundle.split("\n")[0]!.slice(EMBEDDED_METADATA_PREFIX.length),
+ "base64",
+ ).toString("utf-8");
+ expect(metadata).toContain(' "noisy_dag":');
+
+ execFileSync(process.execPath, [bundlePath, "--airflow-metadata"], {
encoding: "utf-8" });
+ });
+
+ it("leaves no bundle behind when the metadata exceeds the embedded size
limit", async () => {
+ outdir = mkdtempSync(path.join(tmpdir(), "ts-pack-"));
+ const entry = path.join(outdir, "huge-entry.ts");
+ writeFileSync(
+ entry,
+ [
+ `import { registerTask, startCoordinator } from
${JSON.stringify(SDK_INDEX)};`,
+ 'for (let i = 0; i < 4000; i += 1) registerTask({ dagId: "big_dag",
taskId: String(i).padStart(240, "t") }, async () => undefined);',
+ "await startCoordinator();",
+ ].join("\n"),
+ );
+
+ await expect(runPack([entry, "--outdir", outdir])).rejects.toThrow(
+ "over the 1048576 byte limit",
+ );
+ expect(existsSync(path.join(outdir, "bundle.mjs"))).toBe(false);
+ expect(existsSync(path.join(outdir,
"bundle.pack-staging.mjs"))).toBe(false);
+ });
+
+ it("leaves no bundle behind when the entry registers no tasks", async () => {
+ outdir = mkdtempSync(path.join(tmpdir(), "ts-pack-"));
+
+ await expect(runPack([EMPTY_ENTRY, "--outdir",
outdir])).rejects.toThrow("registered no tasks");
+ expect(existsSync(path.join(outdir, "bundle.mjs"))).toBe(false);
+ expect(existsSync(path.join(outdir,
"bundle.pack-staging.mjs"))).toBe(false);
+ });
+});
diff --git a/ts-sdk/tests/coordinator/runtime-manifest.test.ts
b/ts-sdk/tests/coordinator/runtime-manifest.test.ts
new file mode 100644
index 00000000000..f6aea6f2fd5
--- /dev/null
+++ b/ts-sdk/tests/coordinator/runtime-manifest.test.ts
@@ -0,0 +1,61 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { AIRFLOW_METADATA_SENTINEL, buildBundleManifest } from
"../../src/coordinator/manifest.js";
+import { startCoordinator } from "../../src/coordinator/runtime.js";
+import { SUPERVISOR_API_VERSION } from "../../src/coordinator/protocol.js";
+
+describe("buildBundleManifest", () => {
+ it("groups registrations by Dag ID under the SDK's schema version", () => {
+ expect(
+ buildBundleManifest([
+ { dagId: "dag_a", taskId: "t1" },
+ { dagId: "dag_b", taskId: "t2" },
+ { dagId: "dag_a", taskId: "t3" },
+ ]),
+ ).toEqual({
+ supervisor_schema_version: SUPERVISOR_API_VERSION,
+ dags: {
+ dag_a: { tasks: ["t1", "t3"] },
+ dag_b: { tasks: ["t2"] },
+ },
+ });
+ });
+});
+
+describe("startCoordinator --airflow-metadata", () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("dumps the manifest to stdout and returns without connecting", async ()
=> {
+ const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
+
+ await startCoordinator({ argv: ["node", "bundle.mjs",
"--airflow-metadata"] });
+
+ expect(write).toHaveBeenCalledTimes(1);
+ const written = String(write.mock.calls[0]![0]);
+ expect(written.startsWith(AIRFLOW_METADATA_SENTINEL)).toBe(true);
+ const payload =
JSON.parse(written.slice(AIRFLOW_METADATA_SENTINEL.length));
+ expect(payload.supervisor_schema_version).toBe(SUPERVISOR_API_VERSION);
+ expect(payload.dags).toEqual({});
+ });
+});