Skip to content
Merged
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,28 @@ xapi workers logs <worker-id> --env production --tail --since 10m
xapi workers logs <worker-id> --env production --request-id <request-id>
```

Web projects can declare their browser build separately from Worker modules.
The CLI preserves supported Wrangler `assets` settings and uploads the files
through xAPI as Cloudflare native static assets:

```json
{
"assets": {
"directory": "dist/client",
"binding": "ASSETS",
"notFoundHandling": "single-page-application",
"runWorkerFirst": ["/api/*"]
}
}
```

`workers plan` shows whether the selected environment has a dedicated hostname.
When `webAppReady` is false, production promotion asks you to review the base
path, root-relative routes, and OAuth callbacks without blocking applications
that deliberately support path-prefix hosting. The current JSON Artifact
transport accepts 12 MiB of decoded Worker modules and static assets per
deployment.

Templates are versioned packages shipped with the CLI, not remote code fetched
during `init`. `persistent-agent` includes buildable source plus KV, D1, R2,
Durable Object, Queue, and Workflow declarations. `push` provisions the
Expand Down Expand Up @@ -650,3 +672,11 @@ current IDs and schemas.
## License

MIT

### Native framework deployment bundles

Framework output can be exported with Wrangler's `deploy --dry-run --outfile
dist/app.worker.bundle` and published through `xapi workers push`. The CLI
retains native module names/types/bytes and separately publishes static Assets.
See [the Workers guide](skills/xapi/guides/workers.md#framework-builds-publish-wranglers-complete-bundle)
for configuration, supported metadata and current transport boundaries.
12 changes: 12 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@
"dependencies": {
"@openai/agents": "0.15.0",
"acorn": "^8.18.0",
"busboy": "1.6.0",
"jsonc-parser": "^3.3.1",
"smol-toml": "^1.8.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/bun": "^1.3.9",
"@types/busboy": "1.5.4",
"@types/node": "^18",
"tsup": "^8.5.1",
"typescript": "^6.0.3"
Expand Down
39 changes: 39 additions & 0 deletions schemas/worker-project.v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,45 @@
}
}
},
"assets": {
"type": "object",
"additionalProperties": false,
"required": ["directory"],
"properties": {
"directory": { "$ref": "#/$defs/projectPath" },
"binding": {
"type": "string",
"pattern": "^[A-Z][A-Z0-9_]{0,63}$"
},
"htmlHandling": {
"enum": [
"auto-trailing-slash",
"force-trailing-slash",
"drop-trailing-slash",
"none"
]
},
"notFoundHandling": {
"enum": ["none", "404-page", "single-page-application"]
},
"runWorkerFirst": {
"oneOf": [
{ "type": "boolean" },
{
"type": "array",
"minItems": 1,
"maxItems": 100,
"items": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"pattern": "^!?/"
}
}
]
}
}
},
"environments": {
"type": "object",
"additionalProperties": false,
Expand Down
2 changes: 1 addition & 1 deletion skills/xapi/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Use granular commands only for multi-step work. Keep the instance ID, terminate

## Hosted Workers

Read `guides/workers.md` before creating, importing, planning, pushing, promoting, rolling back, attaching Cloudflare resources, scheduling tasks, or inspecting logs. Workers are continuously addressable JavaScript applications; Sandbox is ephemeral arbitrary compute. Prefer the project workflow: `workers init`, `workers plan --env preview`, `workers push --env preview`, then `workers promote --to production`. Use `init --from-wrangler` for an existing Cloudflare Worker. Git is optional. `push` builds and uploads an immutable Artifact, uses stable recovery keys, and never silently deletes stateful resources or Secrets; an optional platform-owned ephemeral Sandbox build can produce the same Artifact type. Rollback restores code and compatibility settings, never KV/D1/R2/DO/Queue/Workflow/schedule data or Secret values. Run the provider capability check before provisioning so missing permissions such as D1 Edit are reported precisely. KV, D1, R2, Durable Object, Queue, Workflow, Secret, schedule, managed-domain, observability, and billing data are environment- or Worker-scoped; never assume preview and production share state. Queue messages use the documented route envelope, are delivered at least once, and require an idempotent target route. Only `ACTIVE` means deployment succeeded.
Read `guides/workers.md` before creating, importing, planning, pushing, promoting, rolling back, attaching Cloudflare resources, scheduling tasks, or inspecting logs. Workers are continuously addressable JavaScript applications; Sandbox is ephemeral arbitrary compute. Prefer the project workflow: `workers init`, `workers plan --env preview`, `workers push --env preview`, then `workers promote --to production`. Use `init --from-wrangler` for an existing Cloudflare Worker. Git is optional. `push` builds and uploads an immutable Artifact, including separately declared native static assets, uses stable recovery keys, and never silently deletes stateful resources or Secrets. For web applications, inspect `webAppReady`: path-prefix-aware applications can use fallback routing, while root-relative routes and OAuth callbacks need a dedicated hostname. An optional platform-owned ephemeral Sandbox build can produce the same Artifact type. Rollback restores code and compatibility settings, never KV/D1/R2/DO/Queue/Workflow/schedule data or Secret values. Run the provider capability check before provisioning so missing permissions such as D1 Edit are reported precisely. KV, D1, R2, Durable Object, Queue, Workflow, Secret, schedule, managed-domain, observability, and billing data are environment- or Worker-scoped; never assume preview and production share state. Queue messages use the documented route envelope, are delivered at least once, and require an idempotent target route. Only `ACTIVE` means deployment succeeded.

## Usage Workflow

Expand Down
80 changes: 73 additions & 7 deletions skills/xapi/guides/workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,49 @@ package imports must be bundled by the build. The CLI normalizes and hashes the
complete Artifact before `plan` or `push`, so both commands compare identical
bytes. Existing single-file project configurations remain valid.

### Framework builds: publish Wrangler's complete bundle

For a framework that produces a generated Wrangler configuration (for example
vinext), use that configuration to produce the native upload bundle:

```bash
npm run build
npx wrangler deploy --dry-run --config dist/server/wrangler.json --outfile dist/app.worker.bundle
```

Point the project build output to `dist/app.worker.bundle`; omit `build.main`.
Set `assets.directory` to the framework's client output (for example
`dist/client`). Then use `xapi workers plan --env preview` and
`xapi workers push --env preview`. The build command should run both commands
above. `--dry-run` creates a local artifact; it does not publish outside xAPI.

The CLI reads multipart module names, bytes, MIME types and `main_module` from
Wrangler instead of guessing the output directory's contents. It does not
rename chunks or rewrite imports. Assets are packaged with the artifact and
published using CF's asset upload session before the script is activated.
Compatibility date/flags must match the project's Wrangler configuration.
D1/R2/KV binding names must match declared xAPI resources; native account IDs
and resource IDs are not reused. Secrets are set separately through xAPI.
The artifact also preserves `observability.enabled`.

This adapter currently supports the explicitly mapped metadata above, not every
Wrangler setting. Unmapped metadata fails before artifact upload rather than
being silently discarded. Cron triggers are separate from the upload bundle
and must be configured through xAPI schedules. The granular `workers upload`
command is artifact-only; use the project `push` workflow for coordinated
compatibility, resource, secret and asset handling.

Current xAPI transport limits remain 200 modules / 10 MiB decoded modules and
12 MiB decoded modules plus assets. These are xAPI limits, not a statement of
CF's full native capacity. If exceeded, report the unsupported deployment;
never split a project into unrelated deployments or edit framework output to
work around the limit.

A `PATH_FALLBACK` URL is not a root-hosted Web application URL. Do not rewrite
application routes or configure GitHub callbacks against an invented host.
Use the environment's reported routing state and verify a real reachable
`publicOrigin` with empty `publicBasePath` for a root-hosted acceptance test.

After real preview validation, promote the exact active preview Artifact without
rebuilding it:

Expand Down Expand Up @@ -208,12 +251,30 @@ npx xapi-to workers upload <worker-id> \
--idempotency-key artifact-2026-08-21
```

This directory format is for Worker code modules. HTML, CSS, images, fonts, and
other website files are static assets and use Cloudflare's separate assets
upload protocol; the CLI rejects them here instead of silently dropping them.
Native static-assets upload is not exposed by this xAPI CLI flow yet. Until it
is, bundle small application assets into Worker code through the project's
build step; never bypass xAPI by sending the user's key directly to Cloudflare.
This directory format is for Worker code modules. For a web application, keep
HTML, CSS, images, and fonts in a separate build directory and declare it in
`xapi.worker.json`. `workers push` packages those files into the immutable xAPI
Artifact and the platform completes Cloudflare's native static-assets upload:

```json
{
"build": { "command": "npm run build", "output": "dist/worker" },
"assets": {
"directory": "dist/client",
"binding": "ASSETS",
"htmlHandling": "auto-trailing-slash",
"notFoundHandling": "single-page-application",
"runWorkerFirst": ["/api/*"]
}
}
```

Wrangler imports preserve supported `assets` settings. Cloudflare permits up to
25 MiB per asset and 100,000 assets per version. Asset content stays separate
from Worker modules and is never silently dropped. The current xAPI JSON
Artifact transport accepts at most 12 MiB of decoded modules and assets in one
deployment; split larger sites before upload until the multipart Artifact
transport is available.

Save the returned Artifact `id`, then deploy that exact Artifact to preview:

Expand All @@ -225,7 +286,12 @@ npx xapi-to workers deploy <worker-id> \
--idempotency-key release-candidate-1
```

After deployment, read the environment `publicUrl` instead of constructing a hostname:
After deployment, read the environment `publicUrl` instead of constructing a hostname.
For a web application, `workers plan` reports whether that environment has a
dedicated hostname. Preview path fallback remains useful for API and diagnostic
Workers. A path-prefix-aware application can also use it in production;
root-relative browser URLs and OAuth callbacks require `webAppReady: true`.
Promotion surfaces this as a manual review instead of blocking compatible apps.

```bash
npx xapi-to workers get <worker-id> --format pretty
Expand Down
4 changes: 2 additions & 2 deletions src/commands/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { rollbackWorkerProject } from "../workers-rollback.ts";
import { readWorkerLogs, tailWorkerLogs } from "../workers-logs.ts";
import { formatWorkerMetering } from "../workers-metering-output.ts";
import {
loadWorkerArtifact,
loadWorkerArtifactInput,
WorkerArtifactError,
} from "../workers-artifact.ts";
import {
Expand Down Expand Up @@ -665,7 +665,7 @@ export async function workersCommand(
}
let artifact;
try {
artifact = loadWorkerArtifact(
artifact = await loadWorkerArtifactInput(
resolve(required(flags.file, "--file")),
flags.main,
);
Expand Down
4 changes: 3 additions & 1 deletion src/tests/skill-workers-guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ describe('bundled xAPI Workers skill guide', () => {
expect(guide).toContain('"main": "worker.js"');
expect(guide).toContain('--file dist/');
expect(guide).toContain('--main worker.js');
expect(guide).toContain("separate assets\nupload protocol");
expect(guide).toContain("native static-assets upload");
expect(guide).toContain('"directory": "dist/client"');
expect(guide).toContain('`webAppReady: true`');
expect(guide).not.toContain('--build <build-id>');
});

Expand Down
31 changes: 31 additions & 0 deletions src/tests/workers-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,37 @@ describe("Worker Artifact loader", () => {
expect(() => loadWorkerArtifact(root)).toThrow("--main");
});

test("packages native static assets with MIME types and routing settings", () => {
const root = directory();
const worker = join(root, "worker.mjs");
const assets = join(root, "public");
mkdirSync(assets);
writeFileSync(worker, "export default { fetch() { return new Response('api') } };");
writeFileSync(join(assets, "index.html"), "<h1>hello</h1>");
writeFileSync(join(assets, "logo.png"), Buffer.from([137, 80, 78, 71]));

const artifact = loadWorkerArtifact(worker, undefined, {
directory: assets,
binding: "ASSETS",
notFoundHandling: "single-page-application",
runWorkerFirst: ["/api/*"],
});

expect(artifact.kind).toBe("bundle");
if (!("bundle" in artifact.upload)) throw new Error("expected bundle");
expect(artifact.upload.bundle.assets).toEqual({
binding: "ASSETS",
config: {
notFoundHandling: "single-page-application",
runWorkerFirst: ["/api/*"],
},
files: [
expect.objectContaining({ path: "/index.html", contentType: "text/html" }),
expect.objectContaining({ path: "/logo.png", contentType: "image/png" }),
],
});
});

test("rejects missing relative modules and static website assets", () => {
const root = directory();
writeFileSync(
Expand Down
Loading
Loading